Skip to content

Knowledge — LightRAG

What & why

knowledge = LightRAG (HKUDS library), adopted fresh (greenfield — not a port of the 1.x custom OpenSearch + Neo4j implementation). It's a graph-RAG engine: chunk → LLM entity/relationship extraction → knowledge graph → multi-mode retrieval (naive / local / global / hybrid).

Deployment: a separate service

LightRAG is Python; the api is NestJS. So:

  • LightRAG runs as a separate service (Python, server mode) on Hetzner k8s.
  • agent/knowledge is a thin gateway to it: knowledge-base CRUD, feed documents, proxy retrieval, expose via mcp. The ingestion / query / graph concerns collapse inside LightRAG — they are not our sub-slices.

Storage backend (PENDING decision)

LightRAG abstracts KV / vector / graph storage. Two candidates:

  • Unified Postgres (pgvector + Apache AGE) — app data + KV + vector + graph in one Postgres. Eliminates a separate infra/vector and Neo4j. Needs self-host Postgres on Hetzner (managed Neon likely blocks the AGE extension). Best for small/medium per-base sizes.
  • Neo4j (graph) + dedicated vector DB (Qdrant/Milvus) — for large bases; re-opens Neon for app data.

The choice depends on the expected scale shape (below).

Scale: the wall is ingestion

For millions of files, the constraint is indexing, not storage. LightRAG does per-chunk LLM extraction of entities/relationships → millions of files = huge token cost + time (inherent to graph-RAG). Therefore:

  • indexing must be async / queued (tasks + BullMQ), incremental, rate-limited, token-budgeted;
  • offer a cheap naive vector-RAG path — use the graph only where it earns its cost;
  • multi-tenancy saves you: it's not one giant graph but many per-base graphs (LightRAG workspaces), mostly small → natural sharding. The hard case is millions in a single base.

Backend by scale shape: many small bases → unified PG(pgvector+AGE) is fine; millions in one base → pgvector strains (~few M) and AGE is unproven on huge graphs → prefer Neo4j + Qdrant/Milvus.

Reference (already working in Ranch): cleanslice/ranch/k8s/platform/lightrag/* runs ghcr.io/hkuds/lightrag on Postgres with pgvector + AGE (LIGHTRAG_{KV,VECTOR,GRAPH,DOC_STATUS}_STORAGE=PG*). Note: CNPG's stock image lacks AGE, so the LightRAG DB is a separate Postgres (the gzdaniel/postgres-for-rag image) from the CNPG-managed app DB — until a custom CNPG-with-AGE image. EMBEDDING_DIM is fixed at first index (changing it ⇒ re-index). See GitOps.

WARNING

LightRAG is young (2024). Benchmark at target scale before committing, and pin the version (storage schema changes between releases).

Multi-tenancy via workspace

LightRAG's workspace parameter gives logical isolation within shared storage (a subdirectory for file backends; a prefix / namespace / graph-label for DB backends — verify per backend & version).

Design:

  • 1 workspace = 1 knowledge base.
  • The LightRAG service keeps a pool of LightRAG instances keyed by workspace (lazy-init + LRU/idle evict), all sharing one backend's credentials.
  • Security: api (agent/knowledge) derives the workspace from the authenticated team/kb — never trust a client-supplied workspace — and is the only caller of the internal LightRAG service. Always use a non-empty workspace (empty = shared default → cross-tenant leak).
  • Lifecycle: deleting a KB tears down its workspace (delete-by-doc + backend cleanup, e.g. drop the AGE graph). Cold-start of an instance on a base's first op → account for latency (warm/cache).
python
# LightRAG service (Python), simplified
_pool: dict[str, LightRAG] = {}             # workspace -> instance (LRU)

async def get_rag(workspace: str) -> LightRAG:
    if workspace not in _pool:
        rag = LightRAG(
            working_dir=f"/data/{workspace}",
            workspace=workspace,             # ← isolation
            kv_storage="PGKVStorage",
            vector_storage="PGVectorStorage",
            graph_storage="PGGraphStorage",  # AGE
            # llm / embedding functions ...
        )
        await rag.initialize_storages()
        _pool[workspace] = rag               # + size/idle eviction
    return _pool[workspace]
# endpoints: POST /insert {workspace, docs} ; POST /query {workspace, query, mode}

The api gateway calls these endpoints with workspace = kb_id of the owner.