Starting from version v1.5.0 (currently on the dev branch), LightRAG's file processing pipeline has received a major upgrade:
- Supports multiple file content extraction engines: legacy, native, mineru, docling
- Supports multiple text chunking methods: Fix, Recursive, Vector, Paragraph
- Supports disabling entity-relation extraction for individual files
LightRAG Server introduces an intermediate file-processing format: LightRAG Document. This format supports multimodal data such as tables and images, and also includes the document's section/paragraph metadata, which is convenient for content traceability later.
This document is organized from the perspective of LightRAG Server deployment and use: the quick-start configuration that can be applied directly is given first, followed by configuration syntax for content extraction and chunking, storage / directory layout, deduplication, concurrency, and resume rules. Developers who call the LightRAG class directly via Python should jump to Chapter 11: Python SDK Invocation.
- 1. Quick Start
- 2. Processing Options and Configuration Syntax
- 3. File Parsing Engines
- 4. Multimodal Analysis (VLM)
- 5. Chunker Parameter Configuration (chunk_options)
- 6. Storage and Directory Layout
- 7. Same-name and Duplicate Documents
- 8. Operating the Pipeline: Uploading While It Runs, Stopping, Retrying
- 9. Pipeline Resume Rules at Startup
- 10. Troubleshooting
- 11. Python SDK Invocation
- Appendix A. Notes on Upgrading from Legacy
- Appendix B. Environment Variable Quick Reference
All files are processed using the legacy document parsing and chunking strategy. Either leave LIGHTRAG_PARSER unconfigured, or set it to the following value:
LIGHTRAG_PARSER=*:legacy-FNo reliance on external document parsing services or on VLM vision models. Use the new built-in Native engine to parse docx documents with table (t) and equation (e) modality analysis enabled, paired with the P chunking strategy; other documents use the legacy content extractor paired with the more effective R chunking strategy.
LIGHTRAG_PARSER=*:native-teP,*:legacy-REnabling multimodal processing requires the MinerU file parsing service and a VLM vision recognition model. Use Native to parse docx files; use MinerU to parse pdf, office, and various image files. All of the above files have image (i), table (t), and equation (e) modality analysis enabled and are paired with the P chunking strategy. Other documents fall back to the legacy content extractor paired with the R chunking strategy.
LIGHTRAG_PARSER=*:native-iteP,*:mineru-iteP,*:legacy-R
VLM_PROCESS_ENABLE=true
VLM_LLM_MODEL=kimi-k2.6
MINERU_API_MODE=local
MINERU_LOCAL_ENDPOINT=http://localhost:8000
Pis LightRAG's native chunking strategy; see Paragraph Semantic Chunking for details. For VLM configuration, see Role-based LLM/VLM Configuration Guide.
Each comma-separated item of LIGHTRAG_PARSER is one routing rule, matched against the file extension from left to right; the first usable rule wins. Taking *:native-iteP,*:legacy-R apart:
* : native - iteP
│ │ │
│ │ └─ processing options: i/t/e enable image, table and equation
│ │ analysis; P selects the paragraph-semantic chunker
│ └─ content extraction engine
└─ extension the rule applies to; `*` matches any
So the first rule says "parse anything with native, analyze all three modalities, chunk with P", and the second says "anything the first rule cannot handle falls back to legacy with R chunking". A rule is skipped when the engine does not support that extension, or when an external engine has no endpoint configured — which is exactly how *:native-iteP ends up handling only docx / md / textpack while everything else drops through to legacy.
The full grammar is in §2.3, the option letters in §2.1, and the engines in §3.
A dependency-free smoke test — one .docx, no MinerU, no docling, no VLM:
LIGHTRAG_PARSER=*:native-teP,*:legacy-R1. Check the routing before spending a parse. A malformed LIGHTRAG_PARSER already fails startup (§2.7). Once the server is up, GET /documents/supported_file_types returns the extension allowlist and the engine-to-suffix mapping as the server actually resolved them, which answers "did my rule take effect" without ingesting anything.
2. Get the file in. Either POST /documents/upload (the file is stored under INPUT_DIR and the response carries a track_id), or drop files into INPUT_DIR and call POST /documents/scan. To try different settings on a single file without touching .env, rename it with a hint: report.[native-teP].docx (§2.5).
3. Watch it. GET /documents/track_status/{track_id} for that upload, GET /documents/pipeline_status for the live log, GET /documents/status_counts for the batch view. The status ladder is PENDING → PARSING → ANALYZING → PROCESSING → PROCESSED.
4. Find the artifacts. Everything lands under INPUT_DIR/__parsed__/ (§6.2). For report.[native-teP].docx:
__parsed__/report.[native-teP].docx # archived original, hint preserved
__parsed__/report.docx.parsed/ # canonical name, hint stripped
report.blocks.jsonl # first line is meta, the rest are blocks
report.tables.json # only if the document has tables
report.equations.json # only if the document has equations
report.blocks.assets/ # exported images
5. Verify the parse, in this order:
- The first line of
*.blocks.jsonlis ametarecord. - Content lines carry a sensible heading structure — this is what
Psplits on. - The sidecars you expected exist. A missing one means the document has no such content, or the engine does not emit it (
legacyemits none at all — §3.2). - Each sidecar item you enabled analysis for carries
"llm_analyze_result": {"status": "success"}. Askippedorfailurehere is the answer to "why is my image/table missing from the knowledge graph" (§4.3).
6. Verify chunking and ingest. chunks_count in GET /documents/paginated shows the chunk count; the doc-status record's metadata.parse_engine and metadata.process_options show which engine and options actually ran, which is the fastest way to catch a hint that was silently invalidated (§2.7).
If it failed: the source file stays in INPUT_DIR, so you can fix the configuration and scan again. error_msg on the doc-status record is the diagnosis; §10 maps the common ones. Note that both the engine and the processing options are frozen at enqueue time — changing LIGHTRAG_PARSER or a hint does not affect an existing record, so re-running with different settings means deleting the document and uploading it again (§9.3). To reproduce a parse offline without the server, use python -m lightrag.parser.cli (ParserDebugCLI.md).
LightRAG's file processing configuration is composed of two parts: the content extraction engine determines how the original file is parsed, and the processing options determine whether multimodal analysis is performed after parsing, which chunking method to use, and whether to build a knowledge graph. Typically, the environment variable LIGHTRAG_PARSER is first used to set default rules by file extension, and then a [hint] in the filename overrides individual files. Engine and options can be written in the same configuration fragment, for example docx:native-iet or report.[native-R!].docx.
For backward compatibility, when the configuration is not modified, the upgraded file content extraction behavior remains the original legacy behavior. To enable the new content processing engines, configure as described in this chapter; each engine's own capabilities and settings are in §3.
Processing options control, on a per-file basis, the behavior with respect to multimodal analysis, knowledge graph construction, and text chunking. They can be set as per-rule defaults in LIGHTRAG_PARSER (see §2.4) or overridden for an individual file via a filename hint (see §2.5). All options are optional; defaults are shown in the table below. At most one chunking method (F/R/V/P) is specified per file; the other options can be combined arbitrarily.
| Option | Type | Default | Meaning |
|---|---|---|---|
i |
Multimodal | Off | Enable image analysis (VLM) |
t |
Multimodal | Off | Enable table analysis (VLM) |
e |
Multimodal | Off | Enable equation analysis (VLM) |
! |
Pipeline | Off | Disable entity/relation extraction; do not build the knowledge graph (only the chunks vector index is kept; naive / mix retrieval still works) |
F |
Chunking | Default | Fix / fixed-length chunking: legacy method, splits mechanically by fixed token length or by separator (no chunk overlap when splitting by separator) |
R |
Chunking | - | Recursive / recursive character chunking (RecursiveCharacterTextSplitter@LangChain): takes a list of separators (default ["\n\n","\n","。","!","?",";",","," ",""], ordered from strongest to weakest semantic boundary). Splits by paragraph (double newline) first; if a chunk is still over the token limit, falls back stepwise to single newline → Chinese sentence-ending punctuation (。!?) → Chinese mid-sentence punctuation (;,) → space → per-character split. The default cascade includes Chinese punctuation, letting Chinese / mixed Chinese-English documents split at semantic boundaries. English .?! is deliberately excluded (literal matching would mis-split 0.95 / e.g.). |
V |
Chunking | - | Vector / semantic vector chunking (SemanticChunker@LangChain): first splits text into sentences (the default sentence splitting regex recognizes both English .?! and Chinese 。?!, allowing correct sentence splitting in Chinese / mixed Chinese-English documents), computes embeddings of adjacent sentences, then finds semantic breakpoints based on the specified threshold strategy (e.g., percentile, standard_deviation, or interquartile) for splitting. SemanticChunker itself has no chunk size cap — any semantic chunk that exceeds chunk_token_size is automatically split again by R before persistence (preserving V's non-overlap semantics). This chunking strategy never produces overlapping chunks. |
P |
Chunking | - | Paragraph / paragraph semantic chunking (native); splits by heading first and strictly avoids mixing content from the bottom of the previous heading with content from the next heading, which would break semantics. Suited for chunking documents that can accurately identify headings with a clear heading structure. When the body under the same heading is too long and falls back to R, overlap can be preserved according to CHUNK_P_OVERLAP_SIZE; bridging text between adjacent large tables can also be repeated into the surrounding table chunks within that budget. This chunking method can only be applied to lightrag content stored in the sidecar directory. If lightrag content does not exist, it degrades to chunking with R. This chunking method produces far fewer overlapping chunks than the R or F strategies. |
The global multimodal switch
addon_params["enable_multimodal_pipeline"]is deprecated; the related behavior is now uniformly controlled by the file-leveli/t/eoptions. See Appendix A.
Different characters of processing options take effect at different stages of the pipeline:
| Option | Stage | Description |
|---|---|---|
| i/t/e | Analyzing (multimodal analysis) | Determines whether VLM summarization analysis is invoked on the images / tables / equations in the sidecar. The extraction stage is unaffected: the content extraction engine outputs drawings.json / tables.json / equations.json sidecar files based on what the document actually contains. As a result, simply tweaking the i/t/e options to trigger "re-analysis" can complete VLM later without re-parsing the original file. |
| ! | Extraction (entity-relation extraction) | Skips entity/relation extraction and graph writing; chunks are still written to the vector store to retain naive / mix retrieval capabilities. |
| F/R/V/P | Chunking (text chunking) | Determines which chunking strategy to use; does not affect the output of the parsing stage. |
Modality availability is signaled solely by "whether the sidecar file exists"; the content extraction engine does not need to declare its capabilities in meta. If a given document contains no images/tables/equations, the corresponding sidecar is not written; even if the user has enabled
i/t/e, the corresponding modality is silently skipped, butanalyze_multimodallogs an INFO-level line for that document ([analyze_multimodal] sidecar e:equations empty: doc—id ...), making it easy to diagnose "why didn't the VLM run". This is not an error.
The complete configuration model is as follows:
LIGHTRAG_PARSER=ext:engine-options,ext:engine,*:legacy-R
filename.[ENGINE].ext
filename.[ENGINE-OPTIONS].ext
filename.[-OPTIONS].ext
-
LIGHTRAG_PARSERis the default rule table, matched by file extension, e.g.,pdf:mineru,docx:native-iet. -
The
[hint]in a filename is a single-file override rule, e.g.,paper.[mineru].pdf,memo.[native-R!].docx. -
ENGINEis the content extraction engine. Which one you pick decides what the parse stage can produce at all:Engine What it is Reach for it when legacyplain-text extraction, no sidecars you want the pre-upgrade behavior, or the document is plain text anyway nativebuilt-in structured extractor, fully local, no external service docx/md/textpack, and you wantPchunking or modality analysis without deploying anythingmineruexternal MinerU service PDFs, scanned documents, and office / image formats that need layout and OCR doclingexternal docling-serve an alternative to MinerU; the only built-in path to LaTeX equations Only
native,mineruanddoclingwrite sidecars, so only they can serve thei/t/eoptions and thePchunker. Per-engine capabilities, configuration and deployment are in §3. -
OPTIONSis a string combination of processing options, e.g.,iet,R!,P. The options are ultimately written intoprocess_optionsand read by subsequent pipeline stages. -
The hyphen in
ENGINE-OPTIONSis only used to separate the engine from the options; it is not part of the options themselves. -
When only processing options are specified, it must be written as
[-OPTIONS], e.g.,[-!].[abc]without a hyphen is strictly interpreted as an engine name and will raise an error; it will not fall back to being interpreted as options.
Common combination examples:
LIGHTRAG_PARSER=pdf:mineru-R,docx:native-ietP,*:legacy-R
MINERU_API_MODE=local
MINERU_LOCAL_ENDPOINT=http://localhost:8000
DOCLING_ENDPOINT=http://localhost:5001my-proposal.[native-iet].docx # Use the native engine, enable drawing/table/equation analysis
my-memo.[native-R!].docx # Use the native engine, recursive semantic chunking, disable knowledge graph construction
my-proposal.[-!].docx # Use the default engine, only disable knowledge graph construction
my-proposal.[mineru].docx # Use the MinerU engine, all processing options default
LIGHTRAG_PARSER is used to configure the default content extraction engine for different file extensions; default processing options for the rule can also be appended after the engine:
ext:engine,ext:engine,*:legacy
ext:engine;ext:engine;*:legacy
ext:engine-options
- The left side matches the file extension, not the full filename; write
pdf:mineru, not*.pdf:mineru. - Rules are separated by a semicolon
;(recommended) or a comma,. - Rules are checked left to right; priority rules go in front, with the wildcard rule typically at the end.
- The
-optionssuffix after the engine serves as the defaultprocess_optionsfor files matched by this rule. For example,LIGHTRAG_PARSER=docx:native-ietmeans all.docxfiles default to thenativeengine with image, table, and equation analysis enabled.
Square brackets in the filename can be used to temporarily specify how a single file is processed:
paper.[mineru-R].pdf
slides.[docling].pptx
memo.[native-P].docx
notes.[-R].md
The content inside the square brackets supports three forms:
[ENGINE] # Specify only the engine; processing options use the default or what LIGHTRAG_PARSER provides
[ENGINE-OPTIONS] # Specify both engine and processing options
[-OPTIONS] # Specify only processing options; the engine still follows LIGHTRAG_PARSER / default rules
When parsing the hint, content without a hyphen must match an engine name exactly (mineru / native / docling / legacy); when there is content before a hyphen, the part before the hyphen is the engine and the part after is the options; when starting with a hyphen, it specifies only options. The legacy [OPTIONS] syntax is no longer valid; for example, [iet] must now be written as [-iet].
A chunk-strategy selector (F / R / V / P) — in a LIGHTRAG_PARSER rule or a filename hint — may carry per-strategy chunking parameters in parentheses. Inside the parentheses a comma only separates parameters; rule splitting is parenthesis-aware, so this comma is never mistaken for a rule separator (both ; and , remain valid rule separators, but ; is recommended).
notes.[-R(chunk_ts=800,chunk_ol=80)].md # filename hint
LIGHTRAG_PARSER=pdf:legacy-R(chunk_ts=800,chunk_ol=80);*:legacy-R # rule
Currently supported parameters (canonical name / short alias):
| Parameter | Alias | Strategies | Type | Meaning |
|---|---|---|---|---|
chunk_token_size |
chunk_ts |
F / R / V / P | int (≥ 1) | Per-strategy chunk size |
chunk_overlap_token_size |
chunk_ol |
F / R / P | int (≥ 0) | Overlap between chunks (V has no overlap) |
drop_references |
drop_rf |
P | bool | Drop matching reference blocks before chunking, e.g. paper.[-P(drop_rf=true)].pdf. As a boolean it may be written bare: paper.[-P(drop_rf)].pdf means drop_rf=true |
process_optionsstays a pure selector string; each parameter is applied to that strategy'schunk_options(see §5) while the strategy's other env-derived parameters are kept. Aliases are normalized to their canonical name internally.- Merge priority: the selector still follows "a non-empty filename-hint options string wholesale-overrides the rule options"; parameters overlay per strategy — rule parameters first, then filename-hint parameters (filename wins on a shared key).
- Validation is strict both at startup (
LIGHTRAG_PARSER) and at upload (filename hint): an unknown parameter, a wrong type, an out-of-range value, or a parameter on a strategy that does not support it (e.g.chunk_olonV) all raise a friendly error.
drop_referencesdetection knobsCHUNK_P_REFERENCES_TAIL_N(default0: scan all content blocks; a positive value scans only the last N) /CHUNK_P_REFERENCES_HEADINGS(pipe-separated, defaultReferences\|Bibliography\|参考文献) are env-only and read live at run time. Global default can be set via env varCHUNK_P_DROP_REFERENCES.
LIGHTRAG_PARSERis strictly validated at startup: unknown content extraction engines, malformed extension syntax, explicitly using an unsupported extension, external engines missing endpoint, and illegal characters in processing options all cause startup to fail.- When a wildcard rule matches a certain extension, the engine must pass two usability checks (see
parser_routing._engine_is_usable): (a) the engine's capability table supports that extension; (b) if it is an external engine (mineru/docling), the corresponding endpoint/token environment variable is configured. If either check fails, the rule is skipped and the next rule is matched. For example, in*:mineru;html:docling: MinerU does not support thehtmlextension (condition a fails), sohtmlcontinues to matchdocling; ifMINERU_API_MODE=localbutMINERU_LOCAL_ENDPOINTis not set, all PDFs also skip*:mineruand fall to the next rule (condition b fails). This behavior applies to bothLIGHTRAG_PARSERrule matching and filename hint engine selection. - Filename hints have higher priority than
LIGHTRAG_PARSER. If the engine specified in a hint does not support that extension, the system falls back to the default rules to continue selecting an available engine. - If the filename hint provides a non-empty options string, the hint takes precedence; otherwise the default options of the matching item in
LIGHTRAG_PARSERare used; if neither is provided, all defaults are used. - If no rule is available, the file content extraction falls back to
legacy; iflegacyalso does not support the file extension, an error entry is added to the system and the uploaded file remains in theINPUTdirectory. - At most one of F/R/V/P may appear; repeating the same option has effect only once but does not raise an error.
- Case-sensitive: the chunking options F/R/V/P must be uppercase; other options i/t/e must be lowercase.
- If illegal characters appear inside the square brackets, the entire hint is invalidated, the engine follows the default rules, and the options fall back to
LIGHTRAG_PARSERdefaults or all defaults; a warning is also logged. Pis only effective for structuredLightRAG Documentresults extracted bynative; for thelegacypath or unstructured output, it automatically degrades toRand logs a warning.
| Engine | Description | Supported file formats (extensions) |
|---|---|---|
legacy |
Legacy extraction; content is centrally extracted before joining the pipeline | txt md mdx pdf docx pptx xlsx rtf odt tex epub html htm csv json xml yaml yml log conf ini properties sql bat sh c h cpp hpp py java js ts swift go rb php css scss less |
native |
Built-in intelligent structured content extractor | docx md textpack |
mineru |
External MinerU content extraction engine | pdf docx pptx xlsx png jpg jpeg jp2 webp gif bmp (extensible, see MINERU_ADDITIONAL_SUFFIXES) |
docling |
External Docling content extraction engine | pdf docx pptx xlsx md html xhtml png jpg jpeg tiff webp bmp (extensible, see DOCLING_ADDITIONAL_SUFFIXES) |
mineru and docling are external content extraction engines; before enabling related rules, the services must be running first, and the corresponding endpoint/token must be configured in LightRAG.
For both external engines the row above is the baseline set — what the engine handles out of the box. Their remaining input formats (legacy Office doc / xls / ppt, and for docling also ODF, EPUB, AsciiDoc, LaTeX, CSV, …) depend on components installed on the service side rather than in LightRAG — legacy Office conversion requires LibreOffice there — and for MinerU also on the active MINERU_API_MODE. They are therefore not advertised globally: declare what your own deployment can actually handle with MINERU_ADDITIONAL_SUFFIXES / DOCLING_ADDITIONAL_SUFFIXES (see each engine's section below and the MinerU / Docling blocks in env.example), then route those suffixes with a LIGHTRAG_PARSER rule or a per-file hint — declaring a suffix alone does not make a bare x.doc uploadable.
LightRAG caches the parsing results of the mineru and docling engines locally. Re-uploading the same file usually does not trigger the engine to re-parse the document. To delete the parse cache, you must click the "also delete file" option in the delete-file dialog of the document management interface. Modifying the endpoint addresses and effective extraction parameters of the mineru / docling engines will also invalidate the cache, causing the engine to re-parse the file content on the next upload of the same file.
legacy is the fallback engine for every extension except .textpack, which the routing layer always sends to native. It is also what you get with LIGHTRAG_PARSER unset. It extracts plain text only, which has four consequences worth knowing before you route anything to it:
- It never writes sidecars. Its output is
parse_format=raw, so there is nodrawings.json/tables.json/equations.jsonfor the analyze stage to read. Thei/t/eoptions therefore do nothing on a legacy-parsed document, andPdegrades toRbecause there is no heading structure to split on (§2.7). - It has no raw cache directory, so
LIGHTRAG_FORCE_REPARSE_*does not apply to it (§3.7). - Its only configuration knob is
PDF_DECRYPT_PASSWORD, the password used to open protected PDFs. - A scanned PDF with no text layer fails hard rather than producing an empty document. Route such files to
mineruordocling, which can OCR them.
native is LightRAG's built-in structured content extractor that runs fully locally: it does not depend on external services such as MinerU / Docling, the extraction stage never calls a VLM, and it works out of the box with no deployment. Its runtime dependencies are only python-docx + defusedxml (required); the markdown path additionally relies on the optional cairosvg for SVG rasterization (when missing, the SVG is skipped with a warning and the rest of the content is unaffected). Enabling the opt-in smart_heading engine parameter for docx additionally requires the pinned zh_core_web_sm / en_core_web_sm spaCy models (the spacy runtime ships with the api extra; install the models with lightrag-download-cache --spacy-install — the main Docker image already bundles them); deployments that never enable it need no models, and the smart_heading path also calls the EXTRACT-role LLM during parsing. Setting the DOCX_SMART_HEADING=true env var enables smart_heading by default for .docx files that resolve to the native engine — an explicit native(smart_heading=false) rule/hint opts a file back out — and makes the server verify the spaCy models at startup (fail fast instead of failing on the first parse); the same startup check triggers when a LIGHTRAG_PARSER rule carries native(smart_heading=true) (or its flag shorthand native(smart_heading)). The default applies at upload time only: already-ingested documents keep their persisted engine parameters on re-parse.
Supported extensions: docx / md / textpack. How to enable:
docxandmdstill default tolegacy; select native explicitly, e.g. a default ruleLIGHTRAG_PARSER=docx:native/LIGHTRAG_PARSER=md:native, or a filename hintreport.[native-iet].docx/notes.[native].md(syntax in §2.4 / §2.5).textpackis a native-exclusive extension and is routed to native automatically without a hint/rule.
native parses OOXML directly and recognizes the following structures, writing them to the corresponding sidecars (whether a sidecar is produced depends on the document's actual content; see §6.2):
| Element | Extraction behavior | Sidecar |
|---|---|---|
| Heading levels | Heading 1–9 (inferred from pPr/outlineLvl or the style inheritance chain), feeding the P chunking strategy's heading-based splitting |
blocks.jsonl |
| Paragraphs | Includes hyperlink text and list auto-numbering; tracked changes keep only the final text (deletions removed) | blocks.jsonl |
| Tables | 2D structure, auto-expanding merged cells (colspan/rowspan) and extracting cross-page repeated headers | tables.json |
| Images / drawings | Embedded images exported to a resource directory, with placeholders left in the body | drawings.json + <base>.blocks.assets/ |
| Equations | OMML → LaTeX, distinguishing block-level vs inline | equations.json |
Image export details:
- Embedded images are exported to a
<base>.blocks.assets/directory besideblocks.jsonl, supportingpngjpeggifbmptiffwebpemfwmf. - SVG images: when Word saves an SVG it stores both the vector
.svgand a PNG raster fallback; native docx writes that PNG fallback (reading<a:blip>'sr:embed, which points at the PNG) and does not export the SVG vector original. For downstream VLM consumption PNG is usually sufficient, with no further rasterization needed. (Note this differs from the md path's "SVG rasterized via cairosvg" below: docx simply takes the PNG Word already generated.) - VML / OLE objects (legacy Word images, Visio diagrams, equation-editor previews, etc.): their rendered preview is exported via
v:imagedata, commonly EMF/WMF, landing in the same assets directory; if the relationship is marked as an external link (TargetMode="External"), only the URL is recorded and no bytes are exported. Note: EMF/WMF (and the previews of OLE objects such as Visio) can currently only be "extracted to disk" and cannot enter multimodal analysis — the downstream VLM image analysis accepts only the raster formatspng/jpg/jpeg/gif/webp, and other formats (EMF/WMF/SVG, etc.) are silently skipped (markedskipped; no error, and the rest of the document is unaffected). The exception is equations: they are stored as LaTeX text rather than images and are analyzed by the text (EXTRACT) role rather than the VLM, so they are processed normally.
native docx collects the w14:paraId written by Word 2013+ as a paragraph-level provenance anchor. If a document was produced by LibreOffice / WPS / older Word, or its internal docx XML was edited by hand, some paragraphs will lack paraId, and a one-time notice is logged:
[parse_native] <filename>: N paragraphs lack paraId; Re-saving file in Word 2013+ to regenerate ids.
The affected blocks' positions degrade to [{"type": "paraid", "range": null}]. This is only a notice and does not affect parsing success; if you need precise paragraph provenance, follow the hint and "Save As .docx" in Word 2013+ to regenerate the ids.
Beyond docx, the native engine also supports Markdown:
md: splits by heading (ATX#), recognizes native pipe tables (with header), HTML<table>(with<thead>, preserving colspan/rowspan), block-level equations (a paragraph starting with$$and ending with$$; inline$...$is not recognized), and embedded images (base64 data URLs). Content inside fenced code blocks (```) is kept verbatim and not interpreted. As withdocx,mdstill defaults tolegacy; select native viaLIGHTRAG_PARSER=md:nativeor a filename[native]hint.textpack: a TextBundle-format zip package (markdown body plus a resource directory, conventionallyassets/; the export format of Bear / Ulysses, etc.). Onlynativesupports this extension, so it is routed to native automatically without a hint/rule.- Package structure requirements (the body is located by extension, not a fixed
text.markdownname, so you can pack it with any zip tool):- The body file may have any name, as long as its extension is
.mdor.markdown. - If the package contains a
*.textbundlesubdirectory, at most one is allowed (more than one is an error), and the body is looked up only inside that.textbundlesubdirectory (md files in the root are ignored). - If the package contains no
*.textbundlesubdirectory, the body is looked up only in the package root. - The lookup directory must contain exactly one
.md/.markdownfile: zero or more than one is an error. - The directory holding the body is the "bundle root" (
bundle_root) used for asset resolution.
- The body file may have any name, as long as its extension is
- File-reference images embedded by relative path are resolved relative to the bundle root and may live in any subdirectory (not only
assets/); directory traversal is forbidden (.., absolute paths, or references escaping the bundle root are skipped with a warning), and the resolved bytes must pass an image magic-byte check or they are skipped. Relative-path images in a standalone.md(not a textpack) are not resolved (skipped with a warning).
- Package structure requirements (the body is located by extension, not a fixed
- SVG images (base64 / textpack file / downloaded) are rasterized to PNG via cairosvg before being written to the sidecar; if cairosvg is unavailable or rendering fails, the image is skipped (with a warning). System dependency:
cairosvgis a cffi binding —pip install cairosvg(via theapiextra) always succeeds, but rasterization only actually works if the nativelibcairoshared library is also installed on the host (sudo apt-get install libcairo2on Debian/Ubuntu,sudo dnf install cairoon RHEL/Fedora,brew install cairoon macOS, or the GTK3 runtime on Windows) —pip/uvcannot install system libraries, so this step is never automatic outside the official Docker image. The server probes rasterization at startup and logs a warning iflibcairois missing, so the gap is visible before it surfaces as a per-document warning. - External URL images (
) are downloaded and embedded by default (NATIVE_MD_IMAGE_DOWNLOAD_ENABLEDdefaults totrue); a drawing is always emitted (the fetched asset on success, or an external-link fallback on failure). Downloading allows only globally-routable public IPs (both DNS-resolved IPs and every redirect target are checked, and the socket dials the validated IP directly to defeat DNS rebinding; any ambientHTTP(S)_PROXYis ignored); private / loopback / link-local / reserved / CGNAT (100.64.0.0/10) ranges are all rejected. To allow specific internal ranges, configure a CIDR allowlist viaNATIVE_MD_IMAGE_ALLOWED_NON_PUBLIC_CIDRS. Set the flag tofalseto instead drop external images entirely (no drawing emitted, so a document whose only images are external links produces nodrawings.json).- Downloading is additionally bounded per document, not just per image: total retained image bytes (
NATIVE_MD_IMAGE_MAX_TOTAL_BYTES), remote fetch attempts including redirect hops (NATIVE_MD_IMAGE_MAX_REQUESTS), and a wall clock across all downloads (NATIVE_MD_IMAGE_DOWNLOAD_TOTAL_TIMEOUT). Over-budget remote images degrade to external links with a parse warning rather than failing the document, and each request has a real wall-clock deadline (NATIVE_MD_IMAGE_DOWNLOAD_TIMEOUT) that a slow-trickling peer cannot reset. A download in flight is interrupted byPOST /documents/cancel_pipeline.
- Downloading is additionally bounded per document, not just per image: total retained image bytes (
All of native's NATIVE_* environment variables and the .native_raw/ cache directory apply only to external-image downloading in the markdown / textpack engine; the docx path reads no NATIVE_* variable. The two most common:
LIGHTRAG_FORCE_REPARSE_NATIVE(defaultfalse): discard the.native_raw/cache and re-download external images over the network.NATIVE_MD_IMAGE_DOWNLOAD_ENABLED(defaulttrue): the master switch for external-image downloading; set tofalseto drop all external images.
The remaining download / size / budget / SSRF variables (NATIVE_MD_IMAGE_DOWNLOAD_TIMEOUT / NATIVE_MD_IMAGE_DOWNLOAD_REQUIRED / NATIVE_MD_IMAGE_MAX_BYTES / NATIVE_MD_IMAGE_MAX_SVG_PIXELS / NATIVE_MD_IMAGE_MAX_TOTAL_BYTES / NATIVE_MD_IMAGE_MAX_REQUESTS / NATIVE_MD_IMAGE_DOWNLOAD_TOTAL_TIMEOUT / NATIVE_MD_IMAGE_ALLOWED_NON_PUBLIC_CIDRS) — their meanings and defaults are listed in env.example at the repository root.
Downloaded external images are cached in <file>.native_raw/ (beside .parsed/, analogous to .mineru_raw/.docling_raw), reused directly when re-parsing the same unchanged file instead of going back over the network; the cache is invalidated when the source content or the size / SVG-pixel / CIDR options above change. When the document is deleted (with "also delete file" checked in the delete dialog), this cache directory is removed together with .parsed/.
The LightRAG document processing pipeline supports MinerU as a document parser and offers two MinerU access modes:
officialmode: uses MinerU's cloud API v4 service. You need to register an account at the MinerU official website and create an API-KEY first. Then add the following configuration to LightRAG's.envfile:
MINERU_API_MODE=official
MINERU_API_TOKEN=<your_token>
# MINERU_OFFICIAL_ENDPOINT=https://mineru.net # Default value, usually no need to changelocalmode: uses a locally deployed MinerU service, see ParserServiceDeployment.md §1. After the local MinerU service is started, add the following configuration to LightRAG's.envfile:
MINERU_API_MODE=local
MINERU_LOCAL_ENDPOINT=http://<your_mineru_local_server_ip>:8000Shared parameters, honoured in both modes:
| Env | Default | Meaning |
|---|---|---|
MINERU_LANGUAGE |
ch |
OCR / parsing language |
MINERU_ENABLE_TABLE |
true |
Table recognition. This decides whether tables.json is written at all — one stage earlier than the t option of §2.1, which only decides whether an existing sidecar is analyzed |
MINERU_ENABLE_FORMULA |
true |
Formula recognition; same relationship to the e option |
MINERU_PAGE_RANGES |
(empty) | Page range. official forwards it verbatim and accepts 1-3,5,7-9; local accepts only a single page or one simple range, and a comma list is rejected at startup |
MINERU_ADDITIONAL_SUFFIXES |
(empty) | Extensions this deployment can handle beyond the baseline in §3.1 — see the note there on why declaring one is not enough on its own |
local mode only:
| Env | Default | Meaning |
|---|---|---|
MINERU_LOCAL_ENDPOINT |
http://127.0.0.1:8000 |
mineru-api / mineru-router base URL |
MINERU_LOCAL_BACKEND |
hybrid-auto-engine |
Which backend handles the parse: hybrid-auto-engine (pipeline + VLM, needs a GPU and the matching inference engine), pipeline (CPU-friendly, no VLM step), or vlm-auto-engine |
MINERU_LOCAL_PARSE_METHOD |
auto |
auto / txt / ocr for the pipeline component. Pure VLM backends ignore it — the model handles layout and OCR natively |
MINERU_LOCAL_IMAGE_ANALYSIS |
false |
MinerU's own VLM pass for captions and footnotes — see §4.7, it is not related to the i option. The pipeline backend silently drops the flag |
MINERU_LOCAL_START_PAGE_ID |
0 |
First page to parse |
MINERU_LOCAL_END_PAGE_ID |
99999 |
Last page to parse |
official mode only:
| Env | Default | Meaning |
|---|---|---|
MINERU_API_TOKEN |
— | API key from the MinerU website; required |
MINERU_OFFICIAL_ENDPOINT |
https://mineru.net |
Service endpoint |
MINERU_MODEL_VERSION |
vlm |
Model version requested from the cloud service |
MINERU_IS_OCR |
false |
Cloud-side OCR toggle |
Polling budget and cache:
| Env | Default | Meaning |
|---|---|---|
MINERU_POLL_INTERVAL_SECONDS |
2 |
Interval between task-status polls |
MINERU_MAX_POLLS |
600 |
Maximum polls before giving up; the default budget is about 20 minutes |
MINERU_ENGINE_VERSION |
(empty) | Recorded in the raw-bundle manifest; a mismatch invalidates the cache. Empty skips the check (§6.3) |
LIGHTRAG_FORCE_REPARSE_MINERU |
false |
Bypass the raw cache and re-upload on every parse (§3.7) |
MINERU_BBOX_ATTRIBUTES |
{"origin":"LEFTTOP","max":1000} |
Coordinate system recorded in the sidecar meta. Note the default differs from DOCLING_BBOX_ATTRIBUTES |
Local deployment of the MinerU service (Docker image build, vLLM preload, title-level correction) is described in ParserServiceDeployment.md §1.
The docling content extraction engine requires an external docling-serve service (v1 async API). Minimal configuration:
DOCLING_ENDPOINT=http://localhost:5001DOCLING_ENDPOINT is just the base URL (without /v1/convert/file/async). Currently LightRAG uses Docling's standard pipeline to process files. Users can control the behavior of the Docling pipeline through the following environment variables:
| Env | Default | Meaning |
|---|---|---|
DOCLING_DO_OCR |
true |
OCR master switch |
DOCLING_FORCE_OCR |
true |
Force OCR per page (mandatory for scanned documents; enabling it for non-scanned documents usually also helps improve layout recognition quality) |
DOCLING_OCR_ENGINE |
auto |
OCR engine selection (not recommended to change) |
DOCLING_OCR_PRESET |
auto |
OCR engine preset (not recommended to change) |
DOCLING_OCR_LANG |
(empty) | Set per OCR engine requirements (not recommended to change) |
DOCLING_DO_FORMULA_ENRICHMENT |
false |
Whether to recognize equations in the document and output them in LaTeX format; before enabling, ensure that Docling has downloaded the equation recognition model on the backend (see ParserServiceDeployment.md §2) |
When DOCLING_OCR_ENGINE / DOCLING_OCR_PRESET are not configured, they are equivalent to auto; when DOCLING_OCR_LANG is not configured, no language list is passed to docling-serve, and the OCR engine uses its own default. The parse cache signature is computed from these effective parameters, so "not configured" and "explicitly set to the default value" do not invalidate the cache.
Optional input formats (1 env):
| Env | Default | Meaning |
|---|---|---|
DOCLING_ADDITIONAL_SUFFIXES |
(empty) | Comma-separated suffixes this deployment's docling-serve can handle on top of the baseline set in §3.1, e.g. doc,ppt,xls. Docling's legacy Office support requires LibreOffice on the docling-serve side, so these formats are opted in per deployment instead of being advertised globally |
Notes on DOCLING_ADDITIONAL_SUFFIXES:
- Bare lowercase suffixes separated by
,; a leading dot and surrounding whitespace are tolerated (.DOC=doc). Anything else (*.docwritten out of glob habit, or a;-separated list) is rejected at startup rather than silently ignored. - It only makes the suffix routable to docling; it does not by itself make a bare
x.docuploadable. Pair it with a routing rule (LIGHTRAG_PARSER=doc:docling) or a per-file hint (x.[docling].doc) — otherwise such files still fall through to the defaultlegacyengine and are rejected as an unsupported suffix. Conversely, a rule likedoc:doclingwithout this env fails startup validation, sincedocis not among docling's capabilities. - Read live from the environment, so it takes effect whether it comes from the parent shell or from
.env.
Two polling-budget envs (docling-serve uses server-side long-poll; the client does not sleep extra):
| Env | Default | Meaning |
|---|---|---|
DOCLING_POLL_INTERVAL_SECONDS |
5 |
Poll interval for awaiting parse results |
DOCLING_MAX_POLLS |
240 |
Maximum poll iterations; raises TimeoutError when exceeded;default wait time ≈ 5 × 240 (about 20 minutes) |
Three bundle-cache envs:
| Env | Default | Meaning |
|---|---|---|
DOCLING_ENGINE_VERSION |
(empty) | Docling engine version; version changes invalidate the parse cache |
LIGHTRAG_FORCE_REPARSE_DOCLING |
false |
When set to true/1, the parse cache is not used |
DOCLING_BBOX_ATTRIBUTES |
{"origin":"LEFTBOTTOM"} |
Default coordinate system for Docling layout |
Prerequisites for DOCLING_DO_FORMULA_ENRICHMENT: the docling-serve side must have the code-formula model weights ready. The adapter is dual-track compatible — when enabled, the text field is LaTeX; when disabled, or when missing weights cause text == orig, it falls back to plain text and does not write equations.json. Therefore the default of false is conservative; turn it on only after confirming the model is ready on the deployment side.
Local deployment of docling-serve, including downloading the equation-recognition model required by
DOCLING_DO_FORMULA_ENRICHMENT, is described in ParserServiceDeployment.md §2.
Parameters may also be attached to the engine token to override an external engine's per-file behaviour. They are encoded into the persisted parse_engine field and feed both the engine request and its raw-bundle cache signature (so changing a parameter forces a re-parse rather than reusing a stale bundle).
paper.[mineru(page_range=1-3,language=en,local_parse_method=ocr)].pdf # filename hint
scan.[docling(force_ocr=true)].pdf
report.[native(smart_heading)].docx # bare boolean flag
LIGHTRAG_PARSER=pdf:mineru(language=en);*:legacy-R # rule
Currently supported engine parameters (canonical / alias):
| Engine | Parameter | Alias | Type | Notes |
|---|---|---|---|---|
mineru |
page_range |
pr |
list | One or more page ranges; see the list note below |
mineru |
language |
— | str | OCR / model language (e.g. en, ch) |
mineru |
local_parse_method |
local_pm |
enum | auto / txt / ocr (local mode) |
docling |
force_ocr |
ocr |
bool | true / false; may be written bare: docling(ocr) means docling(force_ocr=true) |
native |
smart_heading |
— | bool | Opt-in docx smart heading discovery (see §3.3); may be written bare (native(smart_heading) means =true); the markdown path warns and ignores it |
page_rangemay contain multiple page segments — write onepage_range=...item per segment. Inside(...)a comma only separates parameters, so a multi-segment list should be written aspage_range=1-3,page_range=5,page_range=7-9, not as the env-var single-string formMINERU_PAGE_RANGES="1-3,5,7-9". A multi-segmentpage_rangerequiresMINERU_API_MODE=official;localmode accepts only a single page/range (for example,page_range=1-3).local_parse_methodis local-only. It only affects the local MinerU request, so it is rejected underMINERU_API_MODE=official(the official API neither sends it nor folds it into the cache key — accepting it would silently do nothing).- A boolean engine parameter may be written bare as a flag to keep rules and filenames short:
native(smart_heading)meansnative(smart_heading=true), anddocling(ocr)meansdocling(force_ocr=true). Only booleans may be bare (mineru(language)is a friendly error), and the persistedparse_engineis always re-encoded in the canonicalkey=valueform (native(smart_heading=true)), so the shorthand never changes a cache signature. To turn a boolean off you still write it explicitly (native(smart_heading=false)). - Engine parameters are only accepted by engines that declare them (
mineru/docling/native); attaching a parameter tolegacy, or an unknown parameter to any engine, is a friendly error. Validation runs at startup (LIGHTRAG_PARSER) and at upload. - Merge priority: engine parameters resolve for the final engine — a rule's engine parameters are dropped when a filename hint selects a different (usable) engine.
parse_engineis stored in hint syntax (e.g.mineru(page_range=1-3)) and shown indoc_statusmetadata so you can see the parse parameters a document used.
The native, mineru and docling engines each keep a raw-artifact cache next to the parsed output, so re-parsing an unchanged file does not repeat the expensive work — a network round trip to an external service, or an image download for markdown. Layout and invalidation rules are in §6.3; what matters when configuring is:
| Engine | Cache directory | What it holds | Force re-parse |
|---|---|---|---|
native |
<base>.native_raw/ |
external images downloaded by the markdown / textpack path | LIGHTRAG_FORCE_REPARSE_NATIVE=true |
mineru |
<base>.mineru_raw/ |
the artifact bundle returned by the MinerU service | LIGHTRAG_FORCE_REPARSE_MINERU=true |
docling |
<base>.docling_raw/ |
the artifact bundle returned by docling-serve | LIGHTRAG_FORCE_REPARSE_DOCLING=true |
legacy |
— | no cache | not applicable |
Each bundle records the engine version and the effective parameter signature in its manifest, so changing the endpoint, an extraction parameter, or MINERU_ENGINE_VERSION / DOCLING_ENGINE_VERSION invalidates the cache on its own. The force flags exist for the case the manifest cannot detect: the service changed while its version string did not. Deleting a document with "also delete file" removes the cache directory along with .parsed/.
Parsing writes sidecars; the ANALYZING stage reads them, sends each item to a model, and writes the result back into the item as llm_analyze_result; the PROCESS stage then builds multimodal chunks from those sidecars. Two consequences follow from that ordering:
- Analysis is re-runnable without re-parsing. The extraction stage is unaffected by
i/t/e— the engine writes whatever the document contains — so enabling a modality later completes the VLM work without touching the original file (§9.3). - A modality with no sidecar is silently a no-op, logged at INFO level. That is not an error; it means the document has no such content, or the engine does not emit it.
VLM_PROCESS_ENABLE is often read as a master switch over all three modalities. It is not — it gates images only:
| Option | Analyzed by | Requires VLM_PROCESS_ENABLE |
Sidecar read |
|---|---|---|---|
i images |
VLM role | Yes | *.drawings.json |
t tables |
EXTRACT role | No | *.tables.json |
e equations |
EXTRACT role | No | *.equations.json |
This is why the recommended preset *:native-teP works with no VLM configured at all. Whether a sidecar exists in the first place also depends on the engine: MINERU_ENABLE_TABLE / MINERU_ENABLE_FORMULA for MinerU, DOCLING_DO_FORMULA_ENRICHMENT for docling, and legacy emits none.
Everything in this section applies only to a document whose process_options include i. A document without i never enters image analysis at all, so it processes normally no matter how many images it carries and no matter how VLM_PROCESS_ENABLE is set.
For a document that does have i, each image runs a filter chain, and the VLM gate sits in the middle of it. Everything before the gate is a skip that leaves the document healthy; the gate itself fails the whole document:
| Order | Condition | Outcome |
|---|---|---|
| 1 | image file not found | item skipped, document continues |
| 2 | not a supported raster format (emf / wmf / svg …) | item skipped, document continues |
| 3 | either side smaller than VLM_MIN_IMAGE_PIXEL (64) |
item skipped, document continues |
| 4 | VLM gate: VLM_PROCESS_ENABLE=false or no VLM role |
document FAILED — error_msg reads "VLM analysis required but VLM role is not available" |
| 5 | image file is empty | document FAILED |
| 6 | larger than VLM_MAX_IMAGE_BYTES (5 MB) |
item skipped — note this check is after the gate, so an oversized image does not escape step 4 |
For the text modalities: a table item with empty content is recorded as skipped with a warning and never reaches format validation; only a table item that has content fails the document when its format is missing or invalid, which indicates a corrupt sidecar. An equation item with empty content fails the document directly.
| Variable | Default | Meaning |
|---|---|---|
MAX_EXTRACT_INPUT_TOKENS |
20480 |
total input budget for one extraction / analysis prompt |
SURROUNDING_LEADING_MAX_TOKENS |
2000 |
per-half cap on the text injected before the item |
SURROUNDING_TRAILING_MAX_TOKENS |
2000 |
per-half cap on the text injected after the item |
MM_EXTRACT_CONTENT_MIN_TOKENS |
100 |
floor reserved for the item's own content |
The surrounding budgets are subtracted from the total, so setting them too high starves the item itself. When that happens the server warns at startup and names the variable to raise; either raise MAX_EXTRACT_INPUT_TOKENS or lower the surrounding pair.
VLM_MAX_IMAGE_BYTES (default 5 MB) and VLM_MIN_IMAGE_PIXEL (default 64) bound what is worth sending: the lower bound exists to skip icons and separator rules rather than pay for a VLM call on them. Analyze-stage concurrency is MAX_PARALLEL_ANALYZE (§8.6), which is independent of the parse and insert stages.
VLM_PROCESS_ENABLE is the pipeline switch and belongs here. Which model serves the VLM role — VLM_LLM_MODEL, VLM_LLM_BINDING, VLM_LLM_BINDING_HOST, VLM_LLM_BINDING_API_KEY, VLM_MAX_ASYNC_LLM, VLM_LLM_TIMEOUT — plus the list of vision-capable providers is owned by Role-based LLM/VLM Configuration Guide and is deliberately not repeated here.
MinerU has a VLM pass of its own, enabled by MINERU_LOCAL_IMAGE_ANALYSIS. It runs at parse time on the MinerU host, improves captions and footnotes inside MinerU's own output, costs GPU on that machine, and is ignored by the pipeline backend. It is unrelated to the i option and to VLM_PROCESS_ENABLE.
- Start with
te. It needs only the EXTRACT role — no new infrastructure, no new failure mode. - Confirm the sidecar items carry
llm_analyze_result.status == "success". - Then add
itogether withVLM_PROCESS_ENABLE=trueand a vision-capable binding.
Doing step 3 in two halves is what produces the failure in §4.3 step 4. And because processing options are frozen at enqueue, a document that already failed that way cannot be rescued by editing configuration and retrying: /documents/reprocess_failed re-runs it with the same i. Either make the VLM available and retry, or delete the document and upload it again without i (§8.3).
process_options selects which chunking strategy (F/R/V/P), while chunk_options decides which parameters that chunker uses. The two responsibilities are orthogonal: the former is a single-character selector, the latter is a structured dictionary.
env vars (read once at startup)
│
▼
addon_params["chunker"] (LightRAG instance field, filled by env with legacy fallback)
│
▼ resolve_chunk_options(addon_params, split_by_character=…, split_by_character_only=…)
│
full_docs[doc_id]["chunk_options"] (frozen at enqueue time, an independent snapshot per file)
│
▼
chunker(tokenizer, content, chunk_token_size, **strategy_kwargs) (dispatched by selector during chunking)
- env vars are loaded into
addon_params["chunker"]during theLightRAG.__init__stage (strategy-specific env is read bydefault_chunker_config(), then_apply_chunk_size_overlayfills in legacy env as a fallback). addon_params["chunker"]is anObservableAddonParamsfield; for Server deployments, you only need env / restart for the new values to take effect. To change it at runtime within the Python process (without restarting) and to do per-file overrides, see Chapter 11: Python SDK Invocation.full_docs.chunk_optionsis frozen atapipeline_enqueue_documentsenqueue time: by default it is assembled byresolve_chunk_options(self.addon_params, ...)on the spot; if the caller passes achunk_optionsargument, it is persisted as-is (SDK usage, see §11.4).- The chunker invocation takes the corresponding sub-dictionary from
full_docs.chunk_optionsand dispatches to F/R/V/P by theprocess_options.chunkingselector.
All variables below are read into addon_params["chunker"] once when LightRAG is instantiated: strategy-specific env is read by default_chunker_config(), while legacy env (CHUNK_SIZE / CHUNK_OVERLAP_SIZE) is filled in by _apply_chunk_size_overlay into slots that neither strategy env nor legacy constructor fields filled. After modifying env, the service must be restarted (or a new LightRAG instance created) for it to take effect; documents already enqueued hold the frozen snapshot and are unaffected.
They are grouped by the strategy they configure. A strategy-specific variable always outranks the global fallback for the same slot.
CHUNK_SIZE—1200, int. Top-levelchunk_token_sizefallback. Ranks below strategy-specific env and belowaddon_params["chunker"]["chunk_token_size"]set on the SDK path.CHUNK_OVERLAP_SIZE—100, int. Overlap fallback. Fills a slot only when the strategy has neither its own env (CHUNK_F_OVERLAP_SIZE/CHUNK_R_OVERLAP_SIZE/CHUNK_P_OVERLAP_SIZE) nor the SDK path'sLightRAG(chunk_overlap_token_size=…).
CHUNK_F_SIZE— unset, int. F's ownchunk_token_size, outranking the top-level fallback (CHUNK_SIZEand the SDK path'sLightRAG(chunk_token_size=…)). When unset, F inherits the top-level resolved value.CHUNK_F_OVERLAP_SIZE— unset, int. F's own overlap, outranking the legacy constructor field andCHUNK_OVERLAP_SIZE.CHUNK_F_SPLIT_BY_CHARACTER— unset (null), str. Pre-split separator.nullor an empty string means split by token window only.CHUNK_F_SPLIT_BY_CHARACTER_ONLY—false, bool. Strict mode: do not re-split by token, raise on an oversized segment instead.
-
CHUNK_R_SIZE— unset, int. R's ownchunk_token_size, outranking the top-level fallback. When unset, R inherits the top-level resolved value. -
CHUNK_R_OVERLAP_SIZE— unset, int. R's own overlap, outranking the legacy constructor field andCHUNK_OVERLAP_SIZE. -
CHUNK_R_SEPARATORS— JSON array string. Default:["\n\n","\n","。","!","?",";",","," ",""]
The separator cascade, ordered from the strongest semantic boundary to the weakest. The default includes Chinese sentence-ending (
。!?) and mid-sentence (;,) punctuation, so Chinese and mixed Chinese-English documents split at semantic boundaries. English.?!is deliberately excluded — matching it literally would cut through numbers and abbreviations.Bounded to 64 entries of at most 256 characters each. The splitter descends one level per remaining separator and re-scans the whole text at every level, so an oversized cascade costs
O(len(separators) × len(text))while splitting nothing extra. The two limits do not behave the same way, and the difference is visible in the output: an entry longer than 256 characters is dropped, not shortened — a lone 300-character separator disappears rather than matching its first 256 — while a list longer than 64 entries is truncated to 64, keeping the trailing char-level""sentinel when the original had one. If nothing survives, the fallback is the consumer's own and is not this variable's default:chunking_by_recursive_charactertakes its documentedseparators=Nonepath, which is the splitter's four-entry English cascade["\n\n", "\n", " ", ""]rather than the nine-entryDEFAULT_R_SEPARATORS, whileload_chunk_separatorsfalls back toDEFAULT_R_SEPARATORSminus the sentinel.For non-HTTP configuration, correction is reported once when the value is cached:
CHUNK_R_SEPARATORSat configuration load, and a supplied or replacedaddon_params['chunker']immediately (or at the first enqueue for a compatible nested in-place mutation). The normalized value is then stored for subsequent documents — in place, so a reference to the nestedrecursive_characterdict obtained through the documented runtime-mutation idiom keeps steering later documents. Direct SDK calls and snapshots persisted by older releases retain their stored value and are bounded silently at execution — including the all-dropped fallback toseparators=None, which is reachable only from a direct SDK call — so one historical bad value cannot create a warning per document.One correction is not a bound at all: an
addon_params['chunker']whoseseparatorsis neither a list/tuple norNonehas the key removed and is reported separately. A barestris why — it satisfiesSequence[str], so bounding it would iterate characters and rewrite one typo into a cascade of 64 single characters that reads as deliberate ever after. Dropping the key routes the chunker to its documentedseparators=Nonepath instead. An HTTP request body is rejected instead: the request model raises on either limit, so/documents/textand friends answer 422 and nothing is dropped, truncated or fallen back to. Same numeric bound everywhere, deliberately different response to breaching it — the caller of an HTTP request is present to read the error, an environment variable's author is not.
-
CHUNK_V_SIZE— unset, int. V's ownchunk_token_size. It is a hard cap: anything over it is re-split through R. Outranks the top-level fallback; when unset, V inherits the top-level resolved value. -
CHUNK_V_BREAKPOINT_THRESHOLD_TYPE—percentile, str. One ofpercentile/standard_deviation/interquartile/gradient. -
CHUNK_V_BREAKPOINT_THRESHOLD_AMOUNT— unset (null), float. Threshold magnitude.nulllets LangChain pick the per-type default (percentile uses 95). -
CHUNK_V_BUFFER_SIZE—1, int. How many adjacent sentences are merged when computing distances. -
CHUNK_V_SENTENCE_SPLIT_REGEX— str. Default:(?<=[.?!])\s+|(?<=[。?!])The sentence split fed to LangChain's
SemanticChunker. The default recognizes English.?!(requiring trailing whitespace, so0.95survives) and Chinese。?!(no whitespace required, matching continuous Chinese text). The env value is the raw regex — no JSON quoting.
CHUNK_P_SIZE—2000(DEFAULT_CHUNK_P_SIZE), int. P's ownchunk_token_size, and the one slot that does not inherit. When unset, P does not fall back to the top-levelCHUNK_SIZE/LightRAG(chunk_token_size=…); the slot always carriesDEFAULT_CHUNK_P_SIZEinstead, because paragraph-semantic merging needs more headroom than the global default to keep related paragraphs together. Override it here when a deployment needs a different ceiling. P's internal ratio constants are algorithmic scales derived in proportion to whatever this resolves to.CHUNK_P_OVERLAP_SIZE— unset, int. P's own overlap, outranking the legacy constructor field andCHUNK_OVERLAP_SIZE. It covers two things: text overlap when long body text inside one JSONL content line falls back to R, and the per-side budget for bridging text copied into the chunks around a large table. It does not make table row-level slices overlap each other.
CHUNK_F_SIZE/CHUNK_R_SIZE/CHUNK_V_SIZEbehave the opposite way fromCHUNK_P_SIZE: left unset they do inherit the top-levelchunk_token_size. That is usually what you want — F is the default global window, R prefers a smaller target so it can split at sentence level, and V, being an advisory ceiling, is usually raised rather than lowered to avoid over-splitting.
The final value of each chunking slot is resolved by a specificity-ordered chain (high → low):
addon_params["chunker"]explicit value — field values explicitly written at construction time or set at runtime via the SDK path (see §11.3). Server-only deployments usually don't hit this tier. Most direct; wins everything.- Strategy-specific env —
CHUNK_F_SIZE/CHUNK_R_SIZE/CHUNK_V_SIZE(per-strategychunk_token_size),CHUNK_F_OVERLAP_SIZE/CHUNK_R_OVERLAP_SIZE/CHUNK_P_OVERLAP_SIZE(overlap),CHUNK_P_SIZE(P-specific). When the corresponding size env is unset, F/R/V inherit the top-levelchunk_token_size. Filled only when the slot is not already occupied by ①. - Legacy constructor fields —
LightRAG(chunk_token_size=…, chunk_overlap_token_size=…); only effective on the SDK path, see §11.2. Strategy-agnostic, "coarse-grained default", fills only the slots still empty. - Legacy env —
CHUNK_SIZE/CHUNK_OVERLAP_SIZE. Final fallback.
Example: CHUNK_R_OVERLAP_SIZE=42 + LightRAG(chunk_overlap_token_size=2) → R sub-dictionary chunk_overlap_token_size=42 (strategy env wins), F / P sub-dictionary chunk_overlap_token_size=2 (no F / P-specific env; the legacy constructor field is filled in).
Special case for P's chunk_token_size: the P chunk_token_size slot does NOT walk the full four-tier chain. When ① is not explicitly provided, it resolves directly via CHUNK_P_SIZE env > DEFAULT_CHUNK_P_SIZE (2000), skipping ③ legacy constructor field LightRAG(chunk_token_size=…) and ④ legacy env CHUNK_SIZE. See the CHUNK_P_SIZE row in §5.2 for the rationale.
Three layers of semantic guarantee:
- Reproducibility: change env, restart — old documents still chunk by the snapshot from the moment they were enqueued; results unchanged.
- Resume consistency: resume branch B (content already extracted, redo chunking by current
process_options) also readsfull_docs.chunk_options, preventing env drift from breaking consistency. - Per-file personalization: callers can pass different
chunk_optionsfor each file (typical usage: a management UI configures separators or V threshold individually for a certain file). These are the input semantics on the SDK path; see §11.4.
addon_params["chunker"] (instance field) keeps the sub-dictionaries of all four strategies as the runtime baseline; full_docs[doc_id]["chunk_options"] is a slim snapshot — at enqueue time, only the strategy sub-dictionary selected by process_options is kept (default F), and the parameters of other strategies are discarded, because the processing stage will not read them. When re-parsing, process_options and chunk_options are rewritten together, avoiding residue of old-strategy parameters.
addon_params["chunker"] full baseline (modifiable at runtime via SDK, affecting subsequent enqueues):
full_docs[doc_id]["chunk_options"] slim snapshot (projected by selector; example below is for process_options="R"):
{
"chunk_token_size": 1200, // common token cap (kept as a top-level fallback)
"recursive_character": { // the only retained strategy sub-dictionary
"chunk_overlap_token_size": 100,
"separators": ["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
}
}selector → sub-dictionary mapping: F → fixed_token, R → recursive_character, V → semantic_vector, P → paragraph_semantic; without a selector, F is the default. Each sub-dictionary corresponds one-to-one with the keyword-only parameters of the corresponding chunker function; when adding new parameters, no dispatcher change is needed, just add a kwarg to the chunker function.
Old documents at enqueue time don't yet have the chunk_options field; during chunking, the dispatcher calls resolve_chunk_options(self.addon_params, process_options=…) per the current process_options to fall back to a slim snapshot. After upgrading, it is recommended to run a reprocess once to give old documents a slim chunk_options snapshot (aligned with the current process_options).
File enqueue and extraction results are written into full_docs:
| Field | Description |
|---|---|
file_path |
Basename of the filename (without directory), preserves the original name provided by the user (including the square-bracket hint), e.g., abc.[native-iet].docx is written as-is. When no valid source is provided, it is saved as unknown_source. The filename hint is not stripped, so the management UI can directly show the user's original naming intent. |
canonical_basename |
The canonicalized basename with the processing hint stripped (e.g., abc.docx). Filename deduplication uses this field as the index key, ensuring abc.docx and abc.[native-iet].docx are treated as the same logical document. |
source_path |
The original path provided at enqueue time (written only when it contains a directory separator or is an absolute path), used by the native / mineru / docling parsers to locate the actual file. |
parse_format |
Content format: pending_parse, raw, lightrag. |
content |
When raw, holds the extracted text; when pending_parse, it is an empty string; when lightrag, holds the complete merged text starting with {{LRdoc}} (concatenated body segments of all type=="content" lines in .blocks.jsonl). At the parse stage, the reuse handler (ReuseParser) strips the prefix and hands it to the chunking_func, going through exactly the same code path as raw. |
content_hash |
MD5 of the content, used for cross-filename deduplication. For parse_format=raw, takes the hash of text after sanitize_text_for_encoding; for parse_format=lightrag, takes the hash of the *.blocks.jsonl file; for parse_format=pending_parse, not written, filled in after extraction completes. |
lightrag_document_path |
When parse_format=lightrag, saves the path to the structured LightRAG Document; new records prefer to save the path relative to INPUT_DIR, e.g., __parsed__/report.docx.parsed/report.blocks.jsonl. Note that the subdirectories and the blocks filename in the path both use the canonicalized basename (without hint). |
parse_engine |
The engine that actually completed extraction: legacy, native, mineru, docling. For files awaiting extraction, can also temporarily store the target engine. |
process_options |
The original processing options string recorded at enqueue time (without engine name and the separator -), e.g., "iet", "R!", "". Downstream stages take this field as the authoritative source for deciding whether to enable image / table / equation analysis (i/t/e), whether to disable knowledge graph construction (!), and the chunking method (F/R/V/P). An empty string is equivalent to all defaults. |
chunk_options |
The frozen snapshot of chunker parameters at enqueue time (slim dictionary: only the strategy sub-dictionary selected by process_options is retained, others discarded). Passed in by the SDK-path caller or assembled by resolve_chunk_options(self.addon_params, process_options=…) from instance fields (containing env defaults) as a fallback (see §5.1). process_options chooses which chunking strategy (F/R/V/P); chunk_options decides which parameters that chunker uses. The downstream process_single_document reads strategy-specific kwargs from this field before chunking; persistence guarantees that old documents behave reproducibly across env changes, resumes, and restarts. Rewritten together with process_options when re-parsing. |
pending_parse indicates the file has been enqueued but extraction is not yet complete. After successful extraction, it is rewritten to raw or lightrag, and content_hash is filled in. On extraction failure, pending_parse and the empty content are kept, making subsequent troubleshooting and retry easier.
The original
file_path(with hint),canonical_basename, andcontent_hashare also synchronized intodoc_status, serving as the deduplication index sources forget_doc_by_file_basename/get_doc_by_content_hash.get_doc_by_file_basenameinternally canonicalizes the input throughcanonicalize_parser_hinted_basenamebefore comparing againstcanonical_basename, soabc.docxandabc.[native-iet].docxalways hit the same document.process_optionsis also mirrored intodoc_status.metadata["process_options"], making it convenient for the management UI to directly display the current file's processing policy.
__parsed__ is the archival and analysis-result directory next to the input directory. It both stores already-processed original documents and the LightRAG Document (lightrag format) files and image assets produced by structured parsing.
- Original file archival: after
legacylocal extraction succeeds and enqueueing finishes, the original file is moved into the sibling__parsed__directory;native/mineru/doclingkeep the original file first for the pipeline to parse, and only move it to__parsed__after successful parsing and writing tofull_docs. When archived, the original filename (including[hint]) is preserved, e.g.,report.[native-iet].docxis archived as__parsed__/report.[native-iet].docx, making it easy to trace the user's original name and processing options. - Analysis result directory: structured parsing results are written into a subdirectory named with the canonicalized filename (with
[hint]removed) plus the.parsedsuffix, avoiding name conflicts with the archived original file and ensuring that the same logical document continues to point to the same directory when the filename hint or processing options change. For example, the analysis results ofreport.docx,report.[native].docx, andreport.[native-iet].docxare all written into__parsed__/report.docx.parsed/. - Analysis result files: the LightRAG Document blocks file and sidecars are named with the canonicalized filename stem, e.g.,
__parsed__/report.docx.parsed/report.blocks.jsonl; the same directory may also containreport.tables.json,report.drawings.json,report.equations.json, and thereport.blocks.assets/image asset directory. Whether a sidecar is generated is determined by the document content: the parser only writes the corresponding file when the document actually contains tables / images / equations. This is the only signal of modality availability — the engine does not need to declare capabilities in meta. Thei/t/eoptions only determine whether the next stage invokes the VLM for summarization analysis on already-existing sidecars. - When parsing fails, the original file is not moved, making it easy to fix the configuration and re-process.
- When
/documents/scanencounters a file with the same name that is alreadyPROCESSED, the input file is treated as already processed and moved to__parsed__, not enqueued as a new document. - When
/documents/scanfinds multiple files that share the same canonicalized name in the same scan, the file that claims the canonical source key first wins (i.e. whichever the streaming directory walk reaches first). A scan-wide UNIQUE claim in the disposable disk spool archives every later variant with a warning before any candidate is persisted. For example, if bothabc.docxandabc.[native].docxexist, only the one reached first is processed. A hint selects the engine and no longer grants scheduling priority. - When duplicate content hashes are found during scanning or parsing, the input file is likewise moved to
__parsed__; thisdoc_statusentry is kept asFAILED duplicatefor tracking. - File moves only act on the current input file and do not overwrite or move existing document source files. If a file with the same name already exists at the destination, the system automatically appends
_001,_002, etc., e.g.,report.pdfis archived asreport_001.pdf,report_002.pdf. If the analysis result directory name is already taken by a regular file, a number is also appended, e.g.,report.docx.parsed_001/.
Three engines keep a raw-artifact bundle beside <canonical filename>.parsed/, so that re-parsing an unchanged file does not repeat the one expensive step that engine performs. All three follow the same design: the bundle directory holds the raw artifacts plus a _manifest.json that doubles as the atomic success marker and the cache key.
| Bundle | Written by | Expensive step it avoids | Contents |
|---|---|---|---|
<base>.native_raw/ |
native (markdown / textpack path) |
downloading and rasterizing external http(s) images |
one file per cached image, named sha256(url)[:16], holding the post-rasterization bytes |
<base>.mineru_raw/ |
mineru |
the upload / poll / download round trip to the MinerU service | content_list.json, plus optional full.md / middle.json / layout.pdf / images/ |
<base>.docling_raw/ |
docling |
the upload / poll / download round trip to docling-serve | <base>.json (DoclingDocument, with pages[].image base64), <base>.md for human inspection, and artifacts/image_*.png referenced by pictures[*].image.uri |
The bundle is a sibling of .parsed/, not a child, so it survives the directory wipe performed before every re-extraction.
Design goals:
- Avoid repeating the expensive step. On re-parse the source file's hash and size are validated against
_manifest.jsonfirst; on a hit the network work is skipped entirely and the stored artifacts are fed straight through the adapter and the sidecar writer. - Preserve diagnostic material. When an engine parses something incorrectly, or a sidecar field looks wrong, the bundle is the place to compare against what the engine actually returned.
- Support object traceability (external engines). MinerU-derived
drawings.json/tables.json/equations.jsonrecordcontent_list.json#/Ninself_ref, which resolves back to the original MinerU object and itspage_idx/bbox. - De-hint the uploaded filename (MinerU). When the source filename carries processing hints such as
[mineru-...]/[-iet], the MinerU API is called with the canonicalized name, so no hint-bearing filename ends up inside the returned bundle.
Docling bundle layout:
__parsed__/<base>.docling_raw/
├── _manifest.json
├── <base>.json # DoclingDocument JSON (contains pages[].image base64)
├── <base>.md # Markdown form, for human inspection
└── artifacts/
└── image_*.png # image assets referenced by pictures[*].image.uri
Lifecycle, identical for all three bundles:
| Operation | Behavior |
|---|---|
| First parse | Fetch the artifacts, then atomically write _manifest.json. For docling that is POST /v1/convert/file/async → long-poll /v1/status/poll/{task_id}?wait=N → GET /v1/result/{task_id} → safe extraction of the zip, rejecting absolute paths and ... |
| Re-parse (cache hit) | Do not call the external service; do not rewrite artifacts; rerun adapter + writer to regenerate the sidecar (this is what makes an adapter upgrade cheap). |
| Re-parse (cache miss) | Clear the directory, then fetch and write the manifest again. |
DELETE /documents with delete_file=True |
*.parsed/, the raw bundle, and the original file are all removed together. |
DELETE /documents with delete_file=False |
All artifacts are preserved; only doc_status and KG data are deleted. |
clear_documents / a full sweep of __parsed__ |
Naturally cleared together. |
| scan cycle | Does not GC orphaned bundles — they are removed only on an explicit user deletion, so a debugging site is never swept away by accident. |
Force re-parse (bypass the cache entirely): LIGHTRAG_FORCE_REPARSE_NATIVE / LIGHTRAG_FORCE_REPARSE_MINERU / LIGHTRAG_FORCE_REPARSE_DOCLING (§3.7).
Concurrency safety: LightRAG mandates canonical_basename uniqueness within a workspace (HTTP 409 on upload / enqueue), and the pipeline serializes work per document, so no bundle is ever written concurrently and no extra lock is needed.
A mismatch on any of the following is a cache miss. Shared by all three bundles:
- source file size or sha256 does not match the manifest;
- the recorded artifacts are missing, or their size / sha256 does not match.
Engine-specific conditions on top of that:
| Engine | Additional invalidation conditions |
|---|---|
native |
the download-options signature changed — the size / SVG-pixel / CIDR options of §3.3 |
mineru |
MINERU_ENGINE_VERSION differs from the recorded engine_version; MINERU_API_MODE differs from the recorded api_mode; the endpoint for the current mode (MINERU_OFFICIAL_ENDPOINT / MINERU_LOCAL_ENDPOINT) differs from the recorded endpoint_signature; content_list.json size or sha256 mismatch; a recorded non-critical file (images, middle.json, …) has a different size |
docling |
DOCLING_ENDPOINT differs from the recorded endpoint_signature; DOCLING_ENGINE_VERSION is set and differs from the recorded engine_version; options_signature differs — covering the tunable env (DOCLING_DO_OCR / DOCLING_FORCE_OCR / DOCLING_OCR_ENGINE / DOCLING_OCR_PRESET / DOCLING_OCR_LANG / DOCLING_DO_FORMULA_ENRICHMENT) and the hard-coded constants pipeline / target_type / to_formats / image_export_mode, which are written into the signature so that changing them later cannot silently reuse an old bundle; main JSON missing or size / sha256 mismatch; any artifacts/ image missing or size mismatch |
"Either side empty means skip" applies to
engine_versionandendpoint_signatureon both external engines. If the field was empty when the manifest was written (for exampleMINERU_ENGINE_VERSIONwas unset at first parse), or if the current environment variable is unset, that particular check is skipped. Consequently, setting a version variable after a bundle already exists does not retroactively invalidate it — use the matchingLIGHTRAG_FORCE_REPARSE_*flag for that.
Uploads, directory scans and the text endpoints all deduplicate. Ingesting the same content twice wastes LLM quota and creates duplicate entities in the knowledge graph, so LightRAG checks two rules — filename and content: hitting either marks the attempt a duplicate, records it as FAILED, and never overwrites the existing document. Every check completes before chunking, entity extraction and any graph write — but that is not the same as "before anything is stored": native / mineru / docling must persist a pending-parse record and then the parse result before a content hash exists at all, so that path briefly holds a record that is subsequently rolled back (see §7.1, §7.2).
Rule one: duplicate filename. Only the filename itself is compared (no directory, no workspace path), so /data/a.pdf, inputs/a.pdf and a.pdf are the same name. Any recognized [engine-options] hint is stripped first, so abc.docx, abc.[native].docx and abc.[native-iet].docx all count as the same name; an unrecognized hint is not stripped, so abc.[draft].docx is still a different name. If doc_status already holds a record under that name, the attempt is a duplicate whatever state that record is in — PENDING, PARSING, ANALYZING, PROCESSING, FAILED or PROCESSED.
Rule two: duplicate content. What is compared is the extracted text, not the raw file bytes. Rename the file, or convert it to another format — as long as the extraction produces the same content it is still a duplicate. The value depends on the content format:
parse_format |
The content hash comes from |
|---|---|
raw |
MD5 of the text after encoding sanitization |
lightrag |
MD5 of the sidecar's *.blocks.jsonl file (relative paths resolve against INPUT_DIR) |
pending_parse |
Not computed yet; filled in once parsing actually completes (so empty content cannot cause a false match) |
That also decides when the content check happens: legacy extracts text locally and can check at enqueue time; native / mineru / docling can only check once parsing has really finished, and at that point the attempt stops before anything is built — no chunking, no entity extraction, no graph writes — and the full_docs row written for this attempt is deleted.
Two additional rules:
- The text endpoints (
/documents/text,/documents/texts) must supply a validfile_source, whose basename is used for the name check; without one they return 400. - Sourceless documents (SDK
insert/ainsertwithoutfile_paths, recorded asfile_path=unknown_source) take no part in name dedup —unknown_sourceis only a placeholder, so two sourceless documents never collide on it — but they are still deduplicated by content: inserting the same text again is still a duplicate. The empty string andno-file-pathbehave the same way.
Neither documents within one enqueue batch nor two concurrently issued enqueues can both slip past the check: the later one is always recognized as a duplicate and recorded as FAILED.
| Symptom | What it means |
|---|---|
Upload returns 409 Document storage already contains '<name>' (Status: …) |
doc_status already holds a record under that name (any status, including FAILED) |
Upload returns 409 Input directory already contains a file with the same canonical basename … |
No record exists, but INPUT_DIR still holds a file with that name |
An extra FAILED row appears with error_msg File name already exists. Original doc_id: …, Status: … |
A batch enqueue or a scan hit the filename rule |
FAILED with error_msg Identical content already exists under another filename. Original doc_id: … |
The content rule was hit |
FAILED with error_msg N existing documents already share this file name … |
A legacy "one filename, several primary records" conflict that has to be repaired by doc id first |
The upload path is fail-fast: the first two cases return 409 outright, writing no file and leaving no trace in doc_status. The other three do leave a record in the document list, but the records have different shapes — do not go looking only for the dup- prefix:
- A duplicate caught at enqueue time (same name, or a content hash that already exists on the
legacypath): a newFAILEDrecord with a doc id prefixeddup-is created, and the original document is untouched. Itsmetadatacarriesduplicate_kind(filename/content_hash),original_doc_idandoriginal_track_id, naming the copy that was kept. - A content duplicate found only after parsing (
native/mineru/doclinghave no content hash at enqueue time): nodup-*record is created. Instead this document's owndoc-*record is flipped toFAILEDin place withmetadata.is_duplicate=trueandduplicate_kind=content_hash, thefull_docsrow written for this attempt is deleted, and the source file is archived into__parsed__. filename_conflict: several primary records already share the filename and the system deliberately does not pick a primary for you, sooriginal_doc_idon this record does not name "the copy that was kept" — repair the conflict first, per §7.3.
- To replace a same-named document with new content: delete it first (
POST /documents/delete_document, or the delete dialog in the WebUI document list), then upload. An upload never overwrites an existing record. - To clear out duplicate records: both the
dup-*rows and an original row flipped toFAILEDafter parsing are inert records — they carry no content, and both/documents/scanand/documents/reprocess_failedskip them, so they neither re-run nor fail again. The only way to remove one is an explicit delete; leaving it in place does not affect retrieval. - To re-run the same file with a different engine or different options: dedup is not the obstacle, the frozen configuration is — see §8.3 and §9.3; this case requires delete + re-upload.
filename_conflict(one name, several records): list the conflicts withGET /documents/source_conflictsand repair them withPOST /documents/source_conflicts/repair, naming theprimary_doc_idto keep (dry-run by default; submit again with the returnedcandidate_count/fingerprint, and a changed candidate set returns 409), or runpython -m lightrag.tools.source_conflict_repairoffline. Scan again once repaired.
/documents/scan applies the same dedup rules, but it faces both sides at once — the files on disk and the records in storage — so it handles same-named files with extra automatic steps, precisely so that "fix the configuration, fix the source file, scan again" works directly instead of requiring a manual delete for every file:
| The record in storage | What the scan does |
|---|---|
| No record under that name | Enqueued as a new document; the whole batch is ordered by file modification time, oldest first |
Already PROCESSED |
Not reprocessed; the source file is archived into __parsed__ and a warning is logged |
FAILED, and content is confirmed to have never been extracted |
The failure record is deleted and the file goes through as a brand-new document — this is what makes "fix the source file and scan again" work |
Interrupted mid-flight (PENDING / PARSING / ANALYZING / PROCESSING) |
Both the record and the source file are kept as-is; the pipeline resumes them without re-extracting |
| A different physical file under the same name / a record with unknown origin / one name with several records | Neither enqueued nor deleted, only archived or reported — these need a human decision, see §7.3 |
Two operational notes:
- If a scan is interrupted (cancelled, crashed, restarted) nothing at all is enqueued. Discovery and enqueue are two separate steps, the source files never leave
INPUT_DIR, and the next scan simply rediscovers them. The one thing that does not come back is a failure record already deleted per row three above — the file re-runs as a new document, but the record kept for human review is gone. SCAN_SPOOL_DIRmust point at real, writable local disk. The candidate list built during a scan is stored underSCAN_SPOOL_DIR, falling back toWORKING_DIR/scan_spool, both further split per workspace. Do not point it at tmpfs (on many Linux hosts/tmpis RAM-backed); set it explicitly whenWORKING_DIRitself is a network volume. If the directory is unusable the scan fails outright and enqueues nothing, and the error names the variable; input files are untouched, so nothing is lost by fixing the configuration and scanning again.
This chapter answers three runtime questions: whether you can keep uploading once the pipeline is running, how to stop it, and what to do after a document fails. Concurrency tuning and request admission limits close the chapter.
| What you want to do | Processing documents | Scan classification phase | Clearing / deleting | Manual retry draining |
|---|---|---|---|---|
| Upload a file / insert text | ✅ allowed | ❌ 409 | ❌ 409 | ❌ 409 |
/documents/scan |
⛔ refused | ⛔ refused | ⛔ refused | ⛔ refused |
| Delete / clear documents | ⛔ refused | ⛔ refused | ⛔ refused | ⛔ refused |
| Query / retrieval | ✅ not refused | ✅ not refused | ✅ not refused |
Key points:
- You can keep uploading during a long batch — no need to wait. This is the most commonly misread part: document processing itself does not block uploads. Once the new document's record is written, the running pipeline picks it up on its own — possibly folded into the current batch, possibly at the batch boundary — and neither case needs anything from you.
- Scan, delete and clear need an idle pipeline. A scan reads storage while deciding what to do with each file on disk (§7.4), and delete/clear tear storage down directly; neither can interleave with concurrent writes. When refused,
/documents/scanreturns HTTP 200 withstatus="scanning_skipped_pipeline_busy"and delete/clear returnstatus="busy"— neither is an error, just retry once things are idle. - Queries are not subject to pipeline admission, but results are not guaranteed during a clear/delete. The query endpoints do not check pipeline state, so they are never refused with a 409 the way an upload is; but
/documents/clearconcurrently drops a dozen storages viaasyncio.gather— text chunks, entity / relation / chunk vectors, the knowledge graph,full_docsanddoc_status— while a query holds no consistent snapshot and does not wait for it to finish, so a query issued mid-clear may see empty results, partial results, or a storage error. "The request is accepted" and "the result is consistent" are different guarantees: wait for a destructive operation to finish before querying. - When an upload is refused, the message identifies the cause — three distinct 409s, one per state:
Document scan is classifying files. …— a scan is in its classification phase.Pipeline is clearing or deleting documents. …— a clear or delete is running.A retry of failed documents is draining the pipeline. …— a manual retry is draining the pipeline (§8.3).
- An upload returning 413 / 429 has nothing to do with pipeline busyness; those are request admission limits, see §8.7. For 503, see §8.5.
POST /documents/cancel_pipeline # no request body
The WebUI entry point is the Cancel button inside the "Pipeline Status" dialog on the document management page, clickable only while the pipeline is running and no cancellation has been requested yet. The endpoint returns {"status": "cancellation_requested" | "not_busy", "message": …}; not_busy means nothing is running and there is nothing to cancel.
A 200 does not mean it has stopped. All it does is set a cancellation-requested flag. Cancellation is cooperative: it takes effect between stages, at batch boundaries, and at the 0.5-second poll inside multimodal analysis — it never interrupts an LLM call already in flight. So expect to wait for the current step to finish; cancellation_requested in GET /documents/pipeline_status confirms the request was received.
Once it stops, documents end up in one of these states:
| Where the document was | State after the stop |
|---|---|
| Already finished | PROCESSED, kept, unaffected |
| Already taken into the batch (parsing / analyzing / extracting, or queued) | FAILED, with error_msg like User cancelled during parse: <filename> |
| Not yet taken into the batch | Stays PENDING, resumes on the next trigger |
A few follow-ups:
- Completed work is not wasted. The LLM cache and any multimodal items that analyzed successfully are flushed to disk, so a later retry hits the cache instead of paying again.
⚠️ Documents markedFAILEDare not retried automatically — you have to recover them with one of the explicit retries in §8.3.- Uploads keep working during a cancellation. Newly uploaded documents are picked up by the next run, once this one has exited.
- When it does not apply: a scan's classification phase does not count as "pipeline busy", so the call returns
not_busythere, and there is currently no endpoint to cancel a scan job — just let it finish. Delete/clear jobs can be cancelled, per document: what was already deleted stays deleted, and the rest is reported as not deleted in the response.
Restarting or killing the server is the other way to stop, with different consequences:
- In-flight documents are left at
PARSING/ANALYZING/PROCESSING; they are not written asFAILED. - Those interrupted states are automatically reset to
PENDINGand re-run the next time the pipeline starts, with no manual step. - But starting the server does not start a run by itself: it takes an upload, or a call to
POST /documents/scan, to trigger one. So documents sitting atPROCESSINGafter a restart are not cause for alarm — just trigger a run.
There are three ways to recover a failed document, widest coverage first:
| Method | What it does | When to use it |
|---|---|---|
POST /documents/scan (the WebUI "Scan/Retry" button) |
First resets every recoverable FAILED back to PENDING, then scans INPUT_DIR for new files; it can also handle failure records whose content was never extracted (delete the record and re-run the file as a new document) |
First choice, widest coverage |
POST /documents/reprocess_failed |
Retries FAILED records from storage only, with no directory discovery; but a document that failed during parsing is re-parsed, which still reads the source file its record points at |
When you do not want to trigger a full directory scan |
| Delete + re-upload | Starts over completely | You need a different engine or different processing options, or neither of the above can recover it |
Four rules you have to know:
- Each retry request grants each document exactly one attempt. Fail again and it stays
FAILEDuntil the next explicit retry — it never spins. - Automatic resume does not touch
FAILED. The run triggered by a new upload only recovers interrupted states (PENDING/PARSING/ANALYZING/PROCESSING);FAILEDneeds one of the two explicit entry points above. - Whether it re-parses depends on whether content was extracted successfully. A document that already has content is not re-parsed: the retry restarts from the multimodal analysis stage, first purging the chunks and graph contributions written by the previous run (details in §9.3) — which is why "fix the VLM configuration / wait out the rate limit, then retry" works. A document that failed during parsing (only a
pending_parseplaceholder infull_docs) is reset toPENDINGand re-enters the parse stage, reading the source file inINPUT_DIRagain and calling the engine again — so retrying after fixing MinerU / Docling works, but such a retry fails again if the source file has been deleted. - A retry does not change the engine or the processing options.
parse_engine,process_optionsandchunk_optionsare frozen into the record at enqueue time; editing.envor a filename hint only affects new uploads.
Which failures a retry fixes, and which need a delete + re-upload:
| Failure cause | Retry in place? |
|---|---|
| Transient LLM / VLM / storage / network failure, or a rate limit | ✅ just retry |
Cancelled by the user (User cancelled during …) |
✅ just retry |
VLM not configured (VLM analysis required but VLM role is not available) |
i), delete and re-upload |
| External parser service down / wrong endpoint | |
You want a different parser engine, chunking strategy, or i/t/e/! |
❌ delete and re-upload (§9.3) |
Bad filename hint or invalid chunk parameters (rows prefixed [File Extraction] that never produced content) |
❌ reprocess_failed skips them; fix the filename and use /documents/scan, or delete the record and re-upload |
A scanned PDF routed to legacy, which extracts no text |
❌ route the suffix to mineru / docling first, then delete and re-upload (§3.2) |
A dup-* duplicate record |
❌ inert record; a retry will not touch it, just delete it (§7.3) |
| Deleting the document returns 409 (missing recovery anchor) | ❌ retrying unchanged is refused again; run audit_kg_integrity(..., apply=True) first |
Two more things about /documents/reprocess_failed. It first freezes ingestion and drains the pipeline to idle — during which uploads return 409 and /documents/scan is refused — then rewrites FAILED back to PENDING with no worker running, and finally resumes normal processing; with many documents this takes a while. It can return 429 (too many unacknowledged retry requests, capped by MAX_UNACKED_MANUAL_RETRIES) or 503 (the fence is up, or a clear/delete is running); a drain that cannot reach idle raises the fence, see §8.5.
Both checkboxes in the delete dialog default to unchecked; they decide whether the on-disk artifacts go away too:
- Neither checked: only storage state is removed — chunks, vectors, graph contributions,
doc_status,full_docs. The source file on disk, the archived copy and.parsed/sidecar under__parsed__, and the external engines' raw artifact bundles are all kept. - "Also delete uploaded files" (API parameter
delete_file=true): additionally removes the source file inINPUT_DIR, the archived copy and<base>.parsed/sidecar under__parsed__, and the<base>.mineru_raw//<base>.docling_raw//<base>.native_raw/raw artifact bundles. If you want to re-run with a different engine, or to make an external engine genuinely re-parse, you must check this, otherwise the re-upload hits the cache and gets the old result back (§3.7, §6.3). - "Also delete extracted LLM cache": additionally clears that document's extraction-stage LLM cache, so a re-upload really re-runs the LLM instead of hitting the cache. Check it when you want to verify the effect of a new model or new prompt.
Some failures leave a workspace in a state where continuing would only be guesswork. The pipeline does not guess; it raises the recovery_required fence, after which every write operation (upload / text / scan / manual retry / delete / clear) returns HTTP 503 until an operator lifts it explicitly. Three situations raise it — of which 1 and 3 depend on cross-process dead-owner detection and therefore only occur on Linux with multi-worker Gunicorn (single-process Uvicorn loses its coordination state with the process):
- A worker died in the middle of
custom_chunks/delete/clear. These may be half-committed, so they cannot simply be re-run. (A deadprocessing/scanowner is re-runnable, is reclaimed silently, and raises no fence.) - A manual retry's drain cannot reach idle. Two ways it gets stuck: the same documents keep coming back with no change in state (re-checking could only spin), and documents the drain can never advance at all — rows holding an unfinished custom-chunk operation, which only
/documents/scan's rollback can resolve. In both cases the reset does not run, the retry request stays unacknowledged (its one attempt is still owed), and the fence message carries a bounded sample of the blocking document ids.recovery_kinddistinguishes them:manual_drain_stalledandmanual_drain_blocked. - Whether an owner is alive cannot be determined. Reclaiming an owner's hold requires proving its process is really dead; a holder record without a process identity can never prove it, so the pipeline does not reclaim on a guess and the fence provides the exit instead.
GET /documents/pipeline_status reports recovery_required (boolean), recovery_kind (coarse reason) and recovery_message (the same text as the 503, with a bounded sample of blocking documents for some reasons).
Lifting the fence:
POST /documents/recovery/force_reset
This is an unsafe manual override — it repairs nothing. Besides the fence it also cancels the workspace's queued manual retry requests, and that is required rather than incidental: while a request is queued /documents/scan refuses to run (a scan runs its own exclusive FAILED reset and may not jump the queue), so clearing only the fence would leave the recovery path just as blocked. The response reports cancelled_manual_retries. No document is lost — failed documents stay FAILED and are handled by the next retry request or by the scan's own reset. Because both halves are required, the call is all-or-nothing: if the queued requests cannot be cancelled the endpoint returns 503 and the fence stays up, so you can retry to complete the recovery rather than have the API report a recovery that did not happen.
Recovery order:
| Reason | Action |
|---|---|
manual_drain_blocked |
POST /documents/recovery/force_reset, then POST /documents/scan — the scan rolls back the unfinished operations and runs the FAILED reset itself, so no separate retry call is needed. |
manual_drain_stalled |
POST /documents/recovery/force_reset, then investigate the documents named in recovery_message — the fence cannot say why they are stuck. Re-issue POST /documents/reprocess_failed once they are resolved. |
A worker died during custom_chunks / delete / clear |
Do not reach for force_reset — follow "Repairing half-committed storage" below. |
Can a restart just fix it? There is exactly one test — is the blocker in memory or in storage? A restart clears only runtime coordination state (the fence and owner records in pipeline_status, the queued manual retry requests in the ingress; none of it is persisted). Anything written to doc_status / full_docs / the storages survives untouched.
| Cause | Does a restart fix it? | Why |
|---|---|---|
| Owner liveness undeterminable | ✅ Yes | The stuck reservation record lives in cross-process shared state, so restarting the process group removes it, and no storage was ever touched — this is the cleanest fix |
manual_drain_stalled |
The fence and the queued requests go away with the restart; if the active rows that "keep coming back unchanged" are dead-process PROCESSING / PARSING / ANALYZING orphans, they are automatically reset to PENDING after the restart and re-run on the next trigger, so the blocker disappears. But a restart diagnoses nothing: if they stall on the same state for another reason, the next /documents/reprocess_failed stalls again |
|
manual_drain_blocked |
❌ No | The blocker is the unfinished custom-chunk journal in doc_status.metadata, which is persisted and survives the restart intact. A restart only clears the fence and the queued requests (still necessary — a queued request makes /scan refuse its own reservation); the actual rollback has to come from POST /documents/scan |
A worker died during custom_chunks / delete / clear |
❌ No | What is half-committed is the storage itself — see below |
A restart is therefore a "gentler force_reset": both clear the fence and the queued requests and neither repairs anything, except that a restart additionally resets interrupted documents to PENDING (which is exactly why a stall often heals itself), at the cost of downtime. Prefer a restart when you can take one; use force_reset when you cannot and have confirmed storage was never touched (causes 2 and 3).
The fence itself is not persisted (it lives in pipeline_status), so restarting the service clears it along with the queued requests. That is what decides where force_reset belongs:
- Use it for causes 2 and 3. In both, the reset never ran and storage was not touched at all — only scheduling is stuck, and stopping the service for that is not worth it.
- Avoid it for cause 1. It clears a flag and repairs nothing; once cleared, every write (including concurrent uploads) resumes immediately against possibly half-committed storage. Both offline tools below already require a stopped service — and stopping it removes the fence anyway, so
force_resetnever needs to appear.
What each offline tool can and cannot repair:
| Tool | Source of truth and what it repairs | What it cannot repair | Cost |
|---|---|---|---|
python -m lightrag.tools.kg_integrity_repair |
Walks graph → chunk source_id → text_chunks → full_doc_id to rebuild missing or unusable full_entities / full_relations anchor rows; can also write empty anchor rows for a document that genuinely owns nothing |
Graph↔VDB drift, doc_status / full_docs themselves, custom-chunk journals, the fence itself |
Never calls the LLM or embedder — effectively free |
lightrag-rebuild-vdb |
Drops and rebuilds entities_vdb / relationships_vdb from the graph and chunks_vdb from text_chunks; also clears reverse orphans (present in the vector store, absent from the graph), which incremental repair cannot do |
Anchor rows, doc_status, the correctness of the graph itself, the fence itself |
A full re-embed — real money |
Two ordering traps; getting them backwards turns repairable data into unrepairable data:
kg_integrity_repairmust run before any further deletion. It can only rebuild anchors from surviving chunk provenance; if a half-finished delete already removed thetext_chunksrows, those contributions become irrecoverable orphans that the tool can only report and will never modify on its own. There is no reason not to run report mode (without--apply) first.lightrag-rebuild-vdbmust run last. It treats the graph andtext_chunksas the truth: graph objects a half-finished delete should have removed will be faithfully re-embedded back into the vector store. It guarantees that vectors are consistent with the graph, not that the graph is correct. So settle the graph side first, then consider rebuilding vectors.
The full order for cause 1 is therefore:
- Stop the service (the fence disappears with it — do not reach for
force_reset). - Run
python -m lightrag.tools.kg_integrity_repair --verbosefor the report first: anchor gaps, plus the irrecoverable orphans that are already beyond saving. Run--applyonly if there are gaps. - Start the service and re-run the operation that did not finish — a whole-document purge is journaled and resumes after the phases it already completed;
clearis simply re-run; acustom_chunksrollback is triggered byPOST /documents/scan. - Once things are stable, if you suspect graph↔VDB drift, stop the service again and run
lightrag-rebuild-vdb, using its read-only consistency check first to decide whether the embedding cost is worth paying.
Both tools must run with the service stopped and the workspace idle (concurrent writes make them read a moving target), and lightrag-rebuild-vdb must use the same .env as the server or the rebuilt vectors land in a different embedding space. See README_KG_INTEGRITY_REPAIR.md and README_REBUILD_VDB.md.
The previous sections are about the correctness question of "who may write"; this set of parameters answers the throughput question of "how many workers run at once". The pipeline has 3 stages, and each stage's worker pool is sized independently:
┌─ parse_queues["native"] ─► [native pool × N1] ─┐ ← legacy shares this pool
PENDING ─►├─ parse_queues["mineru"] ─► [mineru pool × N2] ─┼─► q_analyze ─►[analyzer × N4] ─► q_process ─►[processor × N5]
├─ parse_queues["docling"] ─► [docling pool × N3] ─┤
└─ parse_queues[<3rd-party>] ─► [custom pool] ──┘ ← created dynamically per ParserSpec.queue_group
Parse queues are created dynamically from the registry's ParserSpec.queue_group (one registry snapshot per batch): built-in native/mineru/docling each get a group, legacy shares the native pool (local, no network), and third-party engines can declare their own group and concurrency (see ThirdPartyParser.md). At enqueue time each document's parser engine (from the LIGHTRAG_PARSER default or a filename hint) decides which parse queue it lands in; the parse queues never block one another — a saturated mineru queue does not slow docling or native down. After parsing, everything converges on q_analyze (multimodal analysis) and then q_process (entity/relation extraction + ingestion).
| Environment variable | Default | Role | Tuning advice |
|---|---|---|---|
MAX_PARALLEL_PARSE_NATIVE |
5 |
N1: concurrent workers for native parsing (docx / pdf / txt, all local) | Pure CPU with a low memory footprint; scale with core count |
MAX_PARALLEL_PARSE_MINERU |
2 |
N2: concurrent workers for MinerU parsing | MinerU is GPU/CPU heavy, so 2 is a moderate default. Drop to 1 when resources are tight; 2-3 for a local deployment with enough VRAM; higher against MinerU's official cloud service (subject to its quota) |
MAX_PARALLEL_PARSE_DOCLING |
2 |
N3: concurrent workers for Docling parsing | Docling is equally resource-sensitive, so 2 is a moderate default. Drop to 1 when resources are tight; 2-3 for a local deployment with enough CPU/GPU |
MAX_PARALLEL_ANALYZE |
5 |
N4: concurrent workers for multimodal analysis (VLM image / table descriptions) | Consumes VLM quota directly. Keep ≤ the VLM service's concurrency limit |
MAX_PARALLEL_INSERT |
3 |
N5: concurrent documents in the entity/relation extraction + ingestion stage | MAX_ASYNC_LLM / 3 is a good rule of thumb, in the 2~10 range. Each document triggers many LLM calls here, so too high hits LLM rate limits. The same value also backs an asyncio.Semaphore as a second constraint (worker count equals the semaphore value) |
QUEUE_SIZE_PARSE |
20 |
Input queue length for parse (native/MinerU/Docling) | Rarely needs tuning. The queue holds only lightweight doc_ids (large document bodies are stripped before analyze), and it just bounds how many documents the pipeline pre-dispatches to parse workers |
QUEUE_SIZE_ANALYZE |
100 |
Bounded capacity of the analyze queue (parse → analyze) | Rarely needs tuning. Raise it slightly for very large batches (tens of thousands) to avoid back-pressure at the enqueue side; lower it when memory is tight |
QUEUE_SIZE_INSERT |
4 |
Queue capacity between the analyze and process stages | process is the slowest and most memory-hungry stage, so this queue is deliberately small, giving upstream back-pressure |
A few key points:
- The parse stage is isolated per engine, so mixing native/mineru/docling never lets one slow engine drag another down.
- mineru / docling default to 2: both are resource-heavy, so the default stays moderate. Drop to 1 when resources are tight (avoiding OOM / VRAM contention / failure retries); raise it by hand if you have multiple GPUs or a dedicated parsing server.
MAX_PARALLEL_INSERTis both pool size and semaphore ceiling: the pipeline createsSemaphore(max_parallel_insert)and every process worker takes it before extracting and ingesting. So even if you raise the worker count by hand, this value still caps real concurrency — just tune it directly.- Queue size and back-pressure: the small
QUEUE_SIZE_INSERT=4default is deliberate — process is slow and memory-hungry, so a full queue blocks the analyze stage and back-pressures parse, instead of piling tens of thousands of parse results into memory at once. - How changes take effect: every parameter comes from
.env(or the environment) and is read once when theLightRAGinstance is constructed; restart the service after changing one. - Chunking does not scale with concurrency: chunking runs in a dedicated single-worker thread pool so it does not block the event loop, and its concurrency does not grow with
MAX_PARALLEL_INSERT— raising that will not make chunking faster. A customchunking_funcstill runs on the event loop (its contract allows touching the running loop), so CPU-heavy implementations should callasyncio.to_threadthemselves.
Typical tuning scenarios:
- Many PDFs + local MinerU on a single GPU:
MAX_PARALLEL_PARSE_MINERU=2,MAX_PARALLEL_ANALYZE=5,MAX_PARALLEL_INSERT=3(the defaults; drop MINERU to 1 when VRAM is tight). - Many PDFs + MinerU's cloud service:
MAX_PARALLEL_PARSE_MINERU=3~5(per your cloud quota), everything else default. - Pure docx / txt (native only):
MAX_PARALLEL_PARSE_NATIVE=10, withMAX_PARALLEL_INSERTderived fromMAX_ASYNC_LLM/3. - Visible LLM rate limiting: lower
MAX_PARALLEL_INSERTfirst (the process stage makes many LLM calls per document), thenMAX_PARALLEL_ANALYZE(VLM has its own quota).
Before a document reaches any of the mechanisms above, the server may refuse it outright. These variables decide whether an upload is rejected or queued:
| Variable | Default | How it refuses |
|---|---|---|
MAX_UPLOAD_SIZE |
104857600 (100 MB) |
413 — a single uploaded file is too large |
MAX_REQUEST_BODY_BYTES |
1048576 (1 MiB) |
413 — the raw request body is too large. It applies to all routes, in tiers: ordinary routes take this value, /documents/text and /documents/texts take a built-in 50 MiB when the variable is unset, and /documents/upload derives its limit from MAX_UPLOAD_SIZE + 1 MiB. Setting any positive value explicitly (including the 1 MiB default) applies it uniformly to every non-upload route. 0 disables all of them |
MAX_TEXTS_PER_REQUEST |
0 (off) |
413 — too many texts in a single /documents/texts call |
MAX_PENDING_DOCUMENTS |
0 (off) |
429 — too many documents already in PENDING / PARSING / ANALYZING / PROCESSING |
Their full semantics (including how MAX_UPLOAD_SIZE relates to a reverse proxy's own body limit) are in LightRAG Server.
Three more knobs belong to the pipeline itself rather than to request admission:
| Variable | Default | Meaning |
|---|---|---|
PIPELINE_SCHEDULING_PAGE_SIZE |
500 |
Keyset page size for the doc_status backlog scan; 0 disables paging |
PIPELINE_REQUIRE_STRICT_STORAGE_READS |
false |
Refuse to start when the doc_status backend cannot serve strict reads. This directly decides whether the "delete the failure record and re-run the file as a new document" row in §7.4 can act at all — without a reliable point read, nothing is deleted |
MAX_UNACKED_MANUAL_RETRIES |
64 |
Per-workspace ceiling on published-but-unacknowledged manual retry requests (§8.3) |
Each time apipeline_process_enqueue_documents starts up, it pulls all documents in PARSING / ANALYZING / PROCESSING / PENDING / FAILED to continue processing. The resume path branches by "whether content has been extracted", ensuring that any document, regardless of its previous progress, has an idempotent result when resumed under the current process_options.
The resume rule only applies to documents whose doc_id already exists in doc_status. New files joining the queue require the file dedup logic in §7, to avoid new files squeezing out the records of files whose content has already been successfully extracted.
Read full_docs[doc_id]:
parse_format |
Verdict |
|---|---|
lightrag and lightrag_document_path file exists |
✅ extracted |
raw and content is non-empty |
✅ extracted |
Other (including pending_parse, missing record) |
❌ not extracted |
Go through the full pipeline (registry-dispatched parsing get_parser(engine).parse(...) → analyze_multimodal → chunking → entity extraction), with each stage's behavior determined by full_docs.process_options. This is the normal flow of a "first-time enqueue".
Always skip parsing (do not call parse_* again), restart from the ANALYZING stage, clear old chunks / entities, and redo per the current process_options:
| Sub-step | Behavior |
|---|---|
| Engine comparison | If the engine implied by process_options ≠ full_docs.parse_engine, only warn, do not re-parse. The extracted content is an immutable fact; re-running a different engine would produce inconsistency. To switch engines, delete the whole document and re-upload it. |
| Old chunks / entities / relations cleanup | Read status_doc.chunks_list to collect old chunk id set, call _purge_doc_chunks_and_kg(doc_id, chunk_ids): delete chunk rows from chunks_vdb / text_chunks; reverse-lookup affected entities / relations by entity_chunks / relation_chunks, directly remove entries that have lost all sources from the graph and vector store, and call rebuild_knowledge_from_chunks to rebuild with the remaining chunks for entries still contributed by other documents; finally delete the index rows of this doc in full_entities / full_relations. After purge completes, status_doc.chunks_list = [] / chunks_count = 0 are reset to avoid the subsequent state-machine upsert writing back old IDs. |
analyze_multimodal |
For enabled modalities, every run recomputes the sidecar item analysis and overwrites the existing llm_analyze_result. The LLM analysis cache still applies: a cache hit reuses the previous provider response, so semantic fields usually stay the same and only runtime fields such as analyze_time are rewritten. Cache misses, for example after changing the model or prompt, can produce different saved content. |
| Re-chunk | Pick the strategy by the new process_options.chunking, with parameters read from full_docs.chunk_options (the enqueue snapshot; not overwritten by resume; env changes do not affect old documents that still chunk by the parameters from the moment of enqueue). The LightRAG Document path uses paragraph_semantic when process_options=P, otherwise dispatches to F/R/V by selector. |
| Entity extraction / KG-skip | Determined by the new process_options.skip_kg |
This rule guarantees: when users change
i/t/eand re-upload the same-named document (delete the old doc first, then upload the file with the new hint), multimodal analysis is incrementally filled in; when changingF/R/V/P, chunks and graph are rebuilt; when changing!, KG construction is stopped or restored. Engine changes are considered a "major change", uniformly handled by delete + re-upload, not implicitly happening on the resume path.
| Symptom | Cause | What to do |
|---|---|---|
| Upload rejected as an unsupported type | The extension is not in the live allowlist. Declaring MINERU_ADDITIONAL_SUFFIXES / DOCLING_ADDITIONAL_SUFFIXES alone does not make a bare x.doc uploadable (§3.1) |
Add a routing rule or a per-file hint; confirm with GET /documents/supported_file_types |
Server refuses to start after editing LIGHTRAG_PARSER |
Startup validation is strict: unknown engine, malformed syntax, external engine without an endpoint, or a bad engine / chunk parameter (§2.6, §2.7, §3.6) | Read the startup error — it names the offending rule |
A rule seems ignored; everything is parsed by legacy |
The engine failed a usability check and the rule was skipped: either it does not support that extension, or its endpoint / token is unset (§2.7) | Configure the endpoint, or route the extension to an engine that supports it |
| A filename hint is ignored and a warning is logged | Illegal characters inside the brackets, or the options-only form written without its leading hyphen (§2.5) | Use [-OPTIONS], e.g. report.[-teP].docx |
i / t / e enabled but nothing was analyzed |
The sidecar does not exist — the document has no such content, or the engine does not emit it (MINERU_ENABLE_TABLE / MINERU_ENABLE_FORMULA, DOCLING_DO_FORMULA_ENRICHMENT; legacy emits none) |
Look for the INFO line naming the empty sidecar, and inspect *.parsed/ (§4.2) |
| Document FAILED with "VLM analysis required but VLM role is not available" | i is enabled and an image passed the pre-filters while no VLM is configured (§4.3) |
Set VLM_PROCESS_ENABLE=true with a vision-capable binding, or delete the document and re-upload it with te — retrying will not drop i |
| Some images are analyzed, others silently are not | Non-raster format, below VLM_MIN_IMAGE_PIXEL, or above VLM_MAX_IMAGE_BYTES (§4.3) |
Read the item's llm_analyze_result message; raise the byte cap if the image is legitimate |
| Multimodal items fail on a token budget | The surrounding budgets leave too little for the item itself (§4.4) | Raise MAX_EXTRACT_INPUT_TOKENS, or lower SURROUNDING_LEADING_MAX_TOKENS / SURROUNDING_TRAILING_MAX_TOKENS |
P was requested but the chunks look like R output |
No structured LightRAG Document was produced, so P degraded (§2.7, §3.2) |
Route the file to native / mineru / docling instead of legacy |
Headings are wrong in a .docx, so P splits badly |
The document's Word outline is unreliable | Try native(smart_heading=true) (§3.3) and iterate with the parser CLI |
Log notes that paragraphs lack paraId |
Produced by LibreOffice / WPS / older Word (§3.3) | Informational. Re-save in Word 2013+ only if you need paragraph-level provenance |
| Document is FAILED as a duplicate | Basename dedup (canonicalized, hint stripped) or content-hash dedup (§7.1, §7.2) | Delete the existing document first, or rename the new file; duplicate records (dup-*, or an original row flipped to FAILED after parsing) can only be removed explicitly (§7.3) |
Upload returns 409 |
A document with the same canonical basename already exists in the input directory or in doc-status (§7.2) | Delete it via POST /documents/delete_document, then upload again |
Upload returns 413 or 429 |
An admission limit was hit (§8.7) | See the limits table for which one |
Everything returns 503 |
The recovery_required fence is up (§8.5) |
Follow the recovery order documented in that section |
/documents/scan returns scanning_skipped_pipeline_busy |
The pipeline is busy or scanning, uploads are in flight, or a manual retry is queued (§8.1) | Wait for idle; POST /documents/reprocess_failed is the single-call recovery for a stuck retry request |
Cancel was clicked and a batch of documents turned FAILED |
Documents already taken into the batch are marked FAILED on cancellation (§8.2) | Retry with POST /documents/scan or POST /documents/reprocess_failed; finished documents are unaffected |
| Cancel was clicked but the pipeline is still running | Cancellation is cooperative and never interrupts an LLM call already in flight (§8.2) | Wait for the current stage to end; cancellation_requested in GET /documents/pipeline_status confirms the request arrived |
Documents stuck at PROCESSING / PARSING after a restart |
Interrupted states recover automatically, but starting the server does not itself trigger a run (§8.2) | Call POST /documents/scan once, or upload any file to trigger one |
| Changed the engine or the options, but the output is unchanged | Both the engine and process_options are frozen into the doc-status record at enqueue. Automatic resume and /documents/reprocess_failed reuse the stored values; editing LIGHTRAG_PARSER or a hint affects new uploads only (§8.3, §9.3) |
Delete the document (with "also delete file") and upload it again |
| An external engine keeps returning stale output after fixing its service config | The raw-artifact bundle cache was hit (§6.3) | Set the matching LIGHTRAG_FORCE_REPARSE_* flag (§3.7), or delete the document with "also delete file" |
| A scanned PDF fails with "extracted no usable text" | legacy cannot read a PDF with no text layer (§3.2) |
Route it to mineru or docling with OCR enabled |
| MinerU rejects a multi-segment page range | Multi-segment ranges require official mode; local accepts a single page or one simple range (§3.6) |
Use a single range under local, or switch modes |
| Startup fails asking for spaCy models | DOCX_SMART_HEADING=true, or a rule carrying native(smart_heading=true), triggers a fail-fast model check (§3.3) |
Run lightrag-download-cache --spacy-install, or use the main Docker image, which bundles them |
This chapter targets developers who directly import the LightRAG class for integration, covering runtime APIs, constructor parameters, and removed legacy interfaces that Server deployments don't use. Server users usually don't need to read this chapter.
from lightrag import LightRAG
rag = LightRAG(working_dir="./rag_storage", ...)
await rag.initialize_storages()
await rag.ainsert("text", file_paths="doc.pdf")The following behaviors of this invocation style differ from the Server path: you can change addon_params["chunker"] without restarting the process, you can pass per-file chunk_options into apipeline_enqueue_documents, and you can dynamically override the F strategy's pre-split parameters in an ainsert call.
LightRAG(chunk_token_size=…, chunk_overlap_token_size=…) is tier 3 in §5.3's priority chain: "legacy constructor field". Strategy-agnostic and coarse-grained default, fills only slots still empty:
- Lower priority than
addon_params["chunker"]explicit values (§11.3) and strategy-specific env (§5.2). - Higher priority than the legacy env
CHUNK_SIZE/CHUNK_OVERLAP_SIZE. - The instance fields
self.chunk_token_size/self.chunk_overlap_token_sizeare always back-filled tointafter__post_init__, so legacy paths still reading these two fields (e.g., thechunk_opts.get("chunk_token_size") or self.chunk_token_sizefallback inpipeline.py) continue to work.
addon_params["chunker"] is an ObservableAddonParams field; it can be modified at runtime:
rag.addon_params["chunker"]["recursive_character"]["separators"] = ["##", "\n", " "]After modification, subsequent enqueues get the new defaults; already-enqueued documents keep the snapshot from their enqueue moment (see the three layers of semantic guarantee in §5.3). This is tier 1 of §5.3's priority chain: "addon_params["chunker"] explicit value", winning everything.
Server deployments do not have this capability — after changing env, the service must be restarted for it to take effect.
apipeline_enqueue_documents accepts an optional chunk_options argument. When the caller passes a dict / list[dict], it is projected by the current document's process_options into a slim snapshot (keeping only the corresponding strategy sub-dictionary + top-level chunk_token_size) before being persisted to full_docs[doc_id]["chunk_options"]; when not passed, resolve_chunk_options(self.addon_params, process_options=…) assembles one on the spot. Callers can safely pass the full dictionary — the other strategies' sub-dictionaries will be discarded by the dispatcher and won't pollute the store.
Typical usage:
await rag.apipeline_enqueue_documents(
input=["text A", "text B"],
file_paths=["a.[native-R].txt", "b.txt"],
process_options=["R", ""],
chunk_options=[
{"chunk_token_size": 800, "recursive_character": {"separators": ["\n\n", "\n"]}},
{"chunk_token_size": 1500},
],
)Typical scenarios for per-file personalization: a management UI configures separators or V threshold individually for a certain file; in the future, upload APIs may also accept overrides in form / hint.
Compatibility for not passing file_paths: the core APIs insert / ainsert / apipeline_enqueue_documents still support invocations without file_paths; the file_path of such documents is saved as unknown_source, does not participate in filename dedup, and the document ID continues to be generated from text content.
For apipeline_enqueue_documents's own concurrency constraints, see the state/action table in §8.1.
LightRAG.ainsert(split_by_character=…, split_by_character_only=…) runtime parameters are overridden into chunk_options.fixed_token by resolve_chunk_options at enqueue time:
- A non-
Nonesplit_by_characteroverrides the env default; split_by_character_only=Trueoverrides (Falseis the signature default, indistinguishable from "not specified", so the env default wins).
Only effective for the F strategy; other strategies' sub-dictionaries are unaffected.
The legacy apipeline_enqueue_documents behavior of reprocess_existing_non_processed=True would directly delete non-PROCESSED old records and rebuild them during scan, which conflicts with the rules in §7 / §8; it has been entirely removed. Replacement paths:
- Automatic resume: scan handles same-named files per the rules in §7.4 (archive / resume / delete the record then re-enqueue), uniformly picked up by the resume rules in §9 inside the processing loop.
- Forced refresh: first call
/documents/delete_documentto delete the old document, then upload the same-named new file.
addon_params["enable_multimodal_pipeline"] is deprecated. Multimodal analysis is now selected per document through the i / t / e processing options (§2.1), set either as a rule default in LIGHTRAG_PARSER (§2.4) or via a filename hint (§2.5). There is no global "analyze everything" flag any more, because the modality set a document actually contains is decided by the parsing engine and reported through sidecar existence, not by configuration.
Migration: replace the old global switch with the corresponding letters on your routing rules — for example LIGHTRAG_PARSER=*:native-iteP,*:legacy-R. Note that VLM_PROCESS_ENABLE is a separate, orthogonal switch: it gates image analysis only (tables and equations are analyzed by the EXTRACT role), and with i enabled but no VLM available an image that passes the pre-filters fails the document rather than being skipped.
Documents ingested before the upgrade keep the process_options frozen into their doc_status record at enqueue time; changing rules or hints does not retroactively alter them. To re-process an existing document under new options, delete it and upload it again (§9.3).
With LIGHTRAG_PARSER unset, every extension still routes to the legacy content extractor with the legacy chunking behavior, exactly as before the upgrade. The new engines and chunking strategies are opt-in; see §1 for three ready-to-use presets.
apipeline_enqueue_documents(reprocess_existing_non_processed=...) has been removed; see §11.6 for the replacement paths.
Where to find each family of file-processing variables. This is an index, not a duplicate: the authoritative per-variable comments live in env.example.
| Family | Variables | Documented in |
|---|---|---|
| Routing | LIGHTRAG_PARSER |
§2.3, §2.4, §2.5 |
| Chunking | CHUNK_SIZE, CHUNK_OVERLAP_SIZE, CHUNK_{F,R,V,P}_* |
§5.2 |
| Multimodal | VLM_PROCESS_ENABLE, VLM_MAX_IMAGE_BYTES, VLM_MIN_IMAGE_PIXEL, MAX_EXTRACT_INPUT_TOKENS, SURROUNDING_*_MAX_TOKENS, MM_EXTRACT_CONTENT_MIN_TOKENS |
§4 |
| VLM / role models | VLM_LLM_*, VLM_MAX_ASYNC_LLM |
RoleSpecificLLMConfiguration.md |
| legacy engine | PDF_DECRYPT_PASSWORD |
§3.2 |
| native engine | NATIVE_MD_IMAGE_* |
§3.3 |
| native docx smart_heading | DOCX_SMART_HEADING, DOCX_SMART_* tuning |
§3.3 and the smart_heading block of env.example |
| MinerU | MINERU_* |
§3.4 |
| Docling | DOCLING_* |
§3.5 |
| Parse cache | LIGHTRAG_FORCE_REPARSE_{NATIVE,MINERU,DOCLING}, {MINERU,DOCLING}_ENGINE_VERSION |
§3.7, §6.3 |
| Directories | INPUT_DIR, WORKING_DIR, SCAN_SPOOL_DIR |
§6, §7.4 |
| Concurrency | MAX_PARALLEL_*, QUEUE_SIZE_* |
§8.6 |
| Admission and limits | MAX_UPLOAD_SIZE, MAX_REQUEST_BODY_BYTES, MAX_TEXTS_PER_REQUEST, MAX_PENDING_DOCUMENTS, PIPELINE_*, MAX_UNACKED_MANUAL_RETRIES, SCAN_ENQUEUE_BATCH_SIZE |
§8.7 |
| Query-time (not chunking) | ENABLE_CONTENT_HEADINGS — appends each chunk's heading path when assembling the answer context; it does not change chunk boundaries or stored chunk text |
LightRAG Server |
| Offline / tokenizer | TIKTOKEN_CACHE_DIR |
OfflineDeployment.md |
{ "chunk_token_size": 1200, // common token cap "fixed_token": { // F-specific "chunk_token_size": 1200, // optional; when omitted, inherits the top-level chunk_token_size (seedable via CHUNK_F_SIZE) "chunk_overlap_token_size": 100, "split_by_character": null, "split_by_character_only": false }, "recursive_character": { // R-specific "chunk_token_size": 1200, // optional; when omitted, inherits the top-level chunk_token_size "chunk_overlap_token_size": 100, "separators": ["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] // default cascade includes Chinese punctuation }, "semantic_vector": { // V-specific "chunk_token_size": 1200, // optional hard cap; re-split through R when exceeded "breakpoint_threshold_type": "percentile", // percentile | standard_deviation | interquartile | gradient "breakpoint_threshold_amount": null, // null = LangChain default "buffer_size": 1, "sentence_split_regex": "(?<=[.?!])\\s+|(?<=[。?!])" // default regex handles both English and Chinese sentence-ending punctuation // env/SDK only (CHUNK_V_SENTENCE_SPLIT_REGEX); the REST `chunking.params` // object rejects this key with 422 — see GHSA-32jh-39m7-8x84 (ReDoS) }, "paragraph_semantic": { // P-specific "chunk_token_size": 2000, // when omitted, resolves from CHUNK_P_SIZE or DEFAULT_CHUNK_P_SIZE (2000); // does NOT inherit the common chunk_token_size "chunk_overlap_token_size": 100 // when omitted, inherits the legacy overlap resolution chain } }