Knowledge Graph RAG
An internal assistant that answers questions about a company’s own documents — meeting minutes, financial reports, contracts — by reading both the text and the relationships between the entities inside it. Built at Lintasarta on the in-house DekaLLM platform.

- Corpus
- 500k+ PDFs
- Cold pipeline
- ~30min → ~4min
- Warm
- 10–15s
- Retrieval
- Milvus + FalkorDB
Getting documents into a graph
Ingestion is a four-stage LangGraph flow: OCR, then named-entity recognition, then entity-relation extraction, then graph ingestion. A document arrives from S3 as a scan, and leaves as nodes and edges — organizations, people, assets, and the relations connecting them. Redis holds the organization alias map, so the same company written five different ways across five documents resolves to one node rather than five.
The stage that decides whether any of this is usable is entity resolution. Skip it and the graph technically exists but answers nothing, because the entity you asked about is scattered across duplicates that never join up.
Choosing the graph store by measuring
FalkorDB or Neo4j was a real decision, so I benchmarked both on the same extracted output rather than arguing from reputation:
| FalkorDB | Neo4j | |
|---|---|---|
| Connect | 13.9 ms | 34.2 ms |
| Node ingest (42) | 90.1 ms | 30.0 ms |
| Relationship ingest (171) | 412.7 ms | 1,325.9 ms |
| 1-hop retrieval (54 rels) | 7.69 ms | 18.50 ms |
Neo4j ingests nodes three times faster. FalkorDB wins relationship ingest by 3.2x and one-hop retrieval by 2.4x. This workload is relationship-dense — 171 relationships against 42 nodes for a single document — and every user question is a read, so FalkorDB was the right trade. Worth stating plainly: this is one document’s extracted output, not a scale test. It was enough to settle the question it was asked.
Retrieval, and refusing to make things up
A question seeds dense retrieval in Milvus, and those hits expand one hop through FalkorDB across shared entity keys — so an answer can pull in a document that never matched the query text but is connected to something that did. A cross-encoder reranks the merged set, with a fallback cascade down to seed-only if graph expansion returns nothing useful.
Every citation then passes three-state validation before the answer ships, which exists because the failure that actually matters here isn’t a wrong answer — it’s a confident answer citing a page that doesn’t exist. In a company’s legal archive, a fabricated reference is worse than no answer.

The trace is shown to the user, not hidden: which store was hit, how many hits came back, how long each took, and the subgraph the answer was built from.
The two stores, from the inside
“Cross-store” is easy to claim, so here are both halves of it. The graph carries the structure the extraction pipeline found, and the vector index carries the text it was found in.


Making it fast enough to use
The pipeline started at roughly 30 minutes per document and now runs about 4 minutes cold, 10–15 seconds warm. Most of that came from content-addressed caching in Redis — SHA-256 keyed on input so identical content never gets re-processed, and fail-open so a cache outage degrades speed instead of availability — plus async parallelization and eliminating self-pair comparisons in entity resolution. A determinism check guarantees the same document yields the same graph across runs, which is what makes the cache safe to trust in the first place.
Serving it
A FastAPI service fronts retrieval and answering. Authentication is JWT with HS256 over Postgres-backed accounts, with an admin panel for dataset and user management. The interface is Indonesian throughout, because the people using it work in Indonesian.
Registration doesn’t self-approve: a new account is inert until an admin approves it, and every auth event lands in an append-only activity log. In a system whose whole job is answering questions about internal documents, who asked is as much a record as what was answered.

Stack
Python, LangGraph, FastAPI, Milvus, FalkorDB, Redis, Postgres, S3, Docker. Go for the telemetry collector.