LightRAG is a Retrieval-Augmented Generation (RAG) framework that uses graph-based knowledge representation for enhanced information retrieval. The system extracts entities and relationships from documents, builds a knowledge graph, and uses multiple retrieval modes (local, global, hybrid, mix, naive) for queries.
Top-level directories:
src/.test.sh (preferred test runner), setup/ interactive environment wizard (use make env-* rather than calling setup.sh directly — see Configuration > Setup Wizard Outputs), and release tooling.lightrag/ (see Testing below for layout). Working datasets stay in inputs/, rag_storage/, and temp/; deployment collateral lives in docs/, k8s-deploy/, and compose files.lightrag/)LightRAG) — assembled from mixins (see LightRAG class composition). Hosts ainsert_custom_kg, _insert_done, _process_extract_entities, _refresh_addon_params_cache, and addon_params accessors. Critical: always call await rag.initialize_storages() after instantiation._PipelineMixin — owns the document ingestion pipeline (apipeline_enqueue_documents, apipeline_process_enqueue_documents, apipeline_process_error_documents), the parse_native / parse_mineru / parse_docling parser dispatchers, multimodal analysis, validation, and the worker scaffolding.make_lightrag_doc_content.RoleSpec / RoleLLMConfig / _RoleLLMState / ROLES registry plus _RoleLLMMixin — role normalization, builder registration, wrapper rebuild, runtime config update, queue cleanup, sanitized config export, queue status reporting. Route role-specific behavior here rather than into provider modules._StorageMigrationMixin — check_and_migrate_data, _migrate_entity_relation_data, _migrate_chunk_tracking_storage.ObservableAddonParams plus default_addon_params / normalize_addon_params helpers.BaseKVStorage, BaseVectorStorage, BaseGraphStorage, BaseDocStatusStorage).STORAGE_IMPLEMENTATIONS / STORAGES) lives in kg/__init__.py; kg/factory.py::get_storage_class() resolves backend classes from configuration.parser/routing.py resolves engine and filename hints for legacy, native, mineru, and docling flows; parser/debug.py provides an offline LightRAG stub for the parser/cli.py debug entry point (python -m lightrag.parser.cli). Native format parsers live as sibling sub-packages under parser/ (currently parser/docx/); external HTTP-based adapters live under parser/external/ (mineru, docling) with shared helpers in parser/external/_common.py, _manifest.py, _zip.py.lightrag_server.py) with REST endpoints and Ollama-compatible API; routers under routers/, static Swagger assets, packaged WebUI output, and Gunicorn launcher.LightRAG is assembled from focused mixins (split out of the previously monolithic lightrag.py):
LightRAG → _RoleLLMMixin → _StorageMigrationMixin → _PipelineMixin → object
The @final decorator on LightRAG is preserved — the mixin layering is an internal implementation detail, not an external subclassing surface. The public API (ainsert, aquery, ainsert_custom_kg, initialize_storages, etc.) is unchanged. ainsert_custom_kg and its internal construction logic, _insert_done, _process_extract_entities, _refresh_addon_params_cache, and the addon_params property accessors stay on LightRAG itself because they cut across multiple flows or depend on prompt-profile state.
LightRAG uses 4 storage types with pluggable backends:
Each LightRAG instance can pass a workspace parameter for data isolation. Implementation differs per storage type:
working_dir.The document ingestion pipeline coordinates concurrent writers through pipeline_status (a per-workspace shared dict in lightrag.kg.shared_storage). These fields are mutated under get_namespace_lock("pipeline_status", workspace=...):
busy: any pipeline-busy state. Set by both the processing loop AND destructive jobs (clear / per-doc delete). On its own, busy=True does NOT block enqueue — see destructive_busy for the exclusive subset.destructive_busy: the busy job is /documents/clear or /documents/{doc_id} (delete). These DROP storages and remove input files; a concurrent enqueue accepted in this window would write to storage being torn down and silently lose the document. Reservation and the enqueue last-line guard reject when this is True.scanning: a /documents/scan task is running (whole lifecycle: classification + processing). Used by the /scan endpoint to refuse overlapping scans. Does NOT on its own block uploads/inserts.scanning_exclusive: True only during the scan task's classification phase, when run_scanning_process is reading doc_status to classify files (PROCESSED → archive, FAILED-without-full_docs → retry-as-new, etc.) and possibly deleting stale stubs. Reservation and the enqueue last-line guard reject when this is set. Cleared before the scan transitions to its processing phase, allowing concurrent uploads to land while scan-driven processing finishes.pending_enqueues: count of /upload, /text, /texts endpoints that have reserved a slot (via _reserve_enqueue_slot) but whose bg task has not yet completed. Only the scan endpoint reads this — to refuse starting while uploads are mid-flight.request_pending: a nudge to the running processing loop. Set by either (a) apipeline_process_enqueue_documents when called while busy=True or (b) apipeline_enqueue_documents after writing to doc_status while busy=True. The loop checks it after each batch and re-queries doc_status if set.Mutual-exclusion rules (all checked atomically inside the lock):
| Operation | Refuses if | Writes |
|---|---|---|
_reserve_enqueue_slot |
scanning_exclusive or destructive_busy |
pending_enqueues++ |
apipeline_enqueue_documents (last-line guard) |
(scanning_exclusive and not from_scan) or destructive_busy |
— |
| Scan endpoint reservation | busy or scanning or pending_enqueues > 0 |
scanning = True |
apipeline_process_enqueue_documents entry |
(already busy → set request_pending, return) |
busy = True (NOT destructive_busy) |
clear_documents / delete_document (synchronous reservation) |
busy or scanning or pending_enqueues > 0 |
busy = True, destructive_busy = True |
The contract permits concurrent enqueue + processing: a freshly-uploaded doc lands in doc_status while the loop is mid-batch, the loop sees request_pending after the current batch, re-queries doc_status, and picks up the new PENDING row.
For the rest — write ordering of full_docs vs doc_status, the workspace-scoped enqueue_serialize lock around dedup-and-upsert, and the from_scan=True bypass — see the docstrings on apipeline_enqueue_documents and apipeline_process_enqueue_documents in lightrag/pipeline.py.
# Install with uv
uv sync
source .venv/bin/activate # Or: .venv\Scripts\activate on Windows
# Install with API support
uv sync --extra api
# Install specific extras
uv sync --extra offline-storage # Storage backends
uv sync --extra offline-llm # LLM providers
uv sync --extra test # Testing dependencies
# Copy and configure environment
cp env.example .env # Edit with your LLM/embedding configs
# Build WebUI
cd lightrag_webui
bun install --frozen-lockfile
bun run build
cd ..
# Run server
lightrag-server # Production
uvicorn lightrag.api.lightrag_server:app --reload # Development
lightrag-gunicorn # Multi-worker (gunicorn)
cd lightrag_webui
bun install --frozen-lockfile # Install dependencies
bun run dev # Dev server (Node + Vite)
bun run dev:bun # Dev server (Bun native)
bun run build # Production build
bun run preview # Preview production build
bun run lint # ESLint over *.ts/tsx/js/jsx
# Testing — Bun built-in runner (NOT Vitest/Jest)
bun test # All tests
bun test --watch # Watch mode
bun test --coverage # With coverage report
bun test src/api/lightrag.test.ts # Single test file
Backend tests use pytest; frontend unit tests use Bun's built-in runner — see WebUI above.
# Preferred for fresh shells and automation; resolves PYTHON, venv, uv, .venv, venv, python, python3
./scripts/test.sh tests
# Run specific test file
./scripts/test.sh tests/kg/test_graph_storage.py
# Run with custom workers
./scripts/test.sh tests --test-workers 4
tests/: main test suite, mirrors feature folders. Place new tests under the subdirectory matching the module under test:
tests/api/{auth,config,routes}/ for FastAPI server tests (auth/token, config loading, route handlers); top-level tests/api/ for app-wide concerns (path prefixes, Ollama-compatible endpoint).tests/chunker/, tests/evaluation/, tests/extraction/ for the like-named modules.tests/kg/<backend>_impl/ for backend-specific storage tests, mirroring the lightrag/kg/<backend>_impl.py file naming. The _impl suffix on every subdirectory keeps the layout uniform and avoids sys.path shadowing on names that overlap with top-level PyPI/stdlib packages (faiss, json, neo4j, networkx, redis) when a test is launched directly via python tests/kg/.... Current backends: faiss_impl/, json_impl/, memgraph_impl/, milvus_impl/, mongo_impl/, nano_impl/, neo4j_impl/, networkx_impl/, opensearch_impl/, postgres_impl/, qdrant_impl/, redis_impl/. tests/kg/ root holds cross-backend tests (test_graph_storage, test_batch_graph_operations, test_unified_lock_safety, test_file_atomic).tests/llm/<provider>_impl/ for provider-specific behavior, same _impl convention: bedrock_impl/, gemini_impl/, ollama_impl/, openai_impl/, voyageai_impl/, zhipu_impl/. tests/llm/ root holds cross-provider concerns (embedding, VLM, cache, role).tests/parser/, tests/parser/docx/, tests/parser/external/{mineru,docling}/ for parser implementations.tests/pipeline/ for ingestion pipeline and doc-status behavior (including test_pipeline_*, test_doc_status_*, test_multimodal_*, test_graph_keyed_locks).tests/sidecar/, tests/setup/, tests/workspace/ for the like-named cross-cutting concerns.__init__.py rather than dropping the file in the parent directory root.Markers (see tests/pytest.ini): offline, integration, requires_db, requires_api. Integration tests are skipped by default via -m "not integration".
Integration env vars: LIGHTRAG_RUN_INTEGRATION=true, LIGHTRAG_KEEP_ARTIFACTS=true, LIGHTRAG_TEST_WORKERS=4, plus storage-specific connection strings.
ruff check .
The most common error is forgetting to initialize storages (manifests as AttributeError: __aenter__ or KeyError: 'history_messages'):
import asyncio
from lightrag import LightRAG
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
async def main():
rag = LightRAG(
working_dir="./rag_storage",
llm_model_func=gpt_4o_mini_complete,
embedding_func=openai_embed
)
# REQUIRED: Initialize storage backends
await rag.initialize_storages()
# Now safe to use
await rag.ainsert("Your text here")
result = await rag.aquery("Your question", param=QueryParam(mode="hybrid"))
# Cleanup
await rag.finalize_storages()
asyncio.run(main())
Use @wrap_embedding_func_with_attrs decorator and call .func when wrapping (already-decorated functions cannot be wrapped again — access the underlying via .func):
from lightrag.utils import wrap_embedding_func_with_attrs
@wrap_embedding_func_with_attrs(embedding_dim=1536, max_token_size=8192)
async def custom_embed(texts: list[str]) -> np.ndarray:
# Call underlying function, not wrapped version
return await openai_embed.func(texts, model="text-embedding-3-large")
# Wrong: EmbeddingFunc(func=openai_embed)
# Right: EmbeddingFunc(func=openai_embed.func)
Pitfall — switching embedding models: when changing the embedding model you MUST clear the data directory (optionally keeping
kv_store_llm_response_cache.jsonfor LLM cache). Existing vectors will not match the new model's space.
Configure via environment variables or constructor params:
# Environment-based (recommended for production)
# See env.example for full list
# Constructor-based
rag = LightRAG(
working_dir="./storage",
workspace="project_name", # For data isolation
kv_storage="PGKVStorage",
vector_storage="PGVectorStorage",
graph_storage="Neo4JStorage",
doc_status_storage="PGDocStatusStorage",
vector_db_storage_cls_kwargs={
"cosine_better_than_threshold": 0.2
}
)
# Single document
await rag.ainsert("Text content")
# Batch insertion
await rag.ainsert(["Text 1", "Text 2", ...])
# With custom IDs
await rag.ainsert("Text", ids=["doc-123"])
# With file paths (for citation)
await rag.ainsert(["Text 1", "Text 2"], file_paths=["doc1.pdf", "doc2.pdf"])
# Configure batch size
rag = LightRAG(..., max_parallel_insert=4) # Default: 2, max recommended: 10
from lightrag import QueryParam
result = await rag.aquery(
"Your question",
param=QueryParam(
mode="mix", # Recommended with reranker
top_k=60, # KG entities/relations to retrieve
chunk_top_k=20, # Text chunks to retrieve
max_entity_tokens=6000,
max_relation_tokens=8000,
max_total_tokens=30000,
enable_rerank=True,
user_prompt="Additional instructions for LLM",
stream=False
)
)
For WebUI bugs whose symptoms only surface in the rendered DOM — layout/overflow/scrollbar issues, transient flashes, third-party libraries attaching helpers to <body> outside React's tree, or end-to-end verification of a fix — drive the running dev server (http://localhost:5173) with the document-skills:webapp-testing skill instead of reasoning from source alone. Seed state directly via localStorage (persist key settings-storage, schema in lightrag_webui/src/stores/settings.ts) to skip live LLM calls. Use wait_until="domcontentloaded" plus a selector wait — Vite dev's long-lived polling makes networkidle time out.
Primary configuration file for API server. Generate it with make env-base or copy env.example manually. Key sections:
See env.example for comprehensive template.
.env host-usable. Container-only hostnames and staged SSL paths belong in the wizard-managed compose layer, not persisted back into .env.docker-compose.final.yml as generated output assembled from scripts/setup/templates/*.yml.make env-* targets over direct scripts/setup/setup.sh calls.Comments, backend code, and log messages in English. Frontend uses i18next for multi-language support.
lightrag.utils.logger instead of print@stylistic rules)@typescript-eslint/no-explicit-any is disabled (allowed)HKUDS/LightRAG. Target to HKUDS/LightRAG when creating PRs, not the fork's own repo.