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/knowledgeis a thin gateway to it: knowledge-base CRUD, feed documents, proxy retrieval, expose viamcp. Theingestion/query/graphconcerns 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/vectorand 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/*runsghcr.io/hkuds/lightragon Postgres withpgvector+ AGE (LIGHTRAG_{KV,VECTOR,GRAPH,DOC_STATUS}_STORAGE=PG*). Note: CNPG's stock image lacks AGE, so the LightRAG DB is a separate Postgres (thegzdaniel/postgres-for-ragimage) from the CNPG-managed app DB — until a custom CNPG-with-AGE image.EMBEDDING_DIMis 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).
# 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.