Usage#
This is a hands-on guide to lightrag-agensgraph. It assumes you have completed
Installation — a running AgensGraph with the pgvector extension, and the
AGENSGRAPH_* connection variables set.
You use LightRAG exactly as normal; you just name the AgensGraph storages. Importing
lightrag_agensgraph registers all four. The storages read the AGENSGRAPH_* environment
variables, so set them before you create LightRAG.
Quickstart#
Import the package and tell the storages how to reach AgensGraph:
import os
import asyncio
import lightrag_agensgraph # registers the four AgensGraph storages
from lightrag import LightRAG, QueryParam
from lightrag.kg.shared_storage import initialize_pipeline_status
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
os.environ["AGENSGRAPH_DB"] = "lightrag"
os.environ["AGENSGRAPH_USER"] = "postgres"
os.environ["AGENSGRAPH_PASSWORD"] = "postgres" # must be set; may be empty under trust auth
os.environ["AGENSGRAPH_HOST"] = "localhost"
os.environ["AGENSGRAPH_PORT"] = "5432"
os.environ["OPENAI_API_KEY"] = "sk-..."
Then build LightRAG on the four storages, ingest a document, and query it:
async def main():
rag = LightRAG(
working_dir="./rag_storage", # local working files; data lives in AgensGraph
llm_model_func=gpt_4o_mini_complete,
embedding_func=openai_embed, # text-embedding-3-small, 1536-d
graph_storage="AgensgraphStorage",
vector_storage="AgensgraphVectorStorage",
kv_storage="AgensgraphKVStorage",
doc_status_storage="AgensgraphDocStatusStorage",
)
await rag.initialize_storages() # creates the graph and the tables on first run
await initialize_pipeline_status()
await rag.ainsert("Marie Curie discovered radium and won two Nobel Prizes.")
print(await rag.aquery("What did Marie Curie discover?", param=QueryParam(mode="mix")))
await rag.finalize_storages()
asyncio.run(main())
The four *_storage arguments are independent — set only the ones you want on AgensGraph and
leave the rest at LightRAG’s defaults. Any LightRAG llm_model_func and embedding_func works;
the OpenAI helpers are only the quickest start.
Retrieval modes#
LightRAG’s strength is dual-level retrieval. Ask the same question through every mode to see the difference:
question = "Tell me about Marie Curie and the people, places, and events around her."
for mode in ("naive", "local", "global", "hybrid", "mix"):
answer = await rag.aquery(question, param=QueryParam(mode=mode))
print(f"\n[{mode}]\n{answer}")
Mode |
What it retrieves |
|---|---|
|
Vector search over chunks only (no graph). |
|
Entity-centric: specific entities and their facts. |
|
Relationship-centric: cross-document themes. |
|
|
|
Graph retrieval and chunks together — the default, most thorough. |
To see what a mode retrieved (the dual-level signal — entities, relationships, chunks, and
the high-/low-level keywords), use aquery_data; to get the retrieved context without an LLM
answer, pass only_need_context=True:
result = await rag.aquery_data(question, param=QueryParam(mode="mix"))
data = result["data"]
print(len(data["entities"]), "entities,", len(data["relationships"]), "relationships")
ctx = await rag.aquery(question, param=QueryParam(mode="mix", only_need_context=True))
If you haven’t configured a rerank model, pass
enable_rerank=FalseinQueryParamto silence a no-op rerank warning.
Incremental ingestion and document status#
New documents extend the same graph; entities recurring across documents merge into one
node. Tag each ingestion wave with a track_id and give documents stable ids so a re-ingested
document is not extracted again. The doc-status pipeline tracks every document.
from lightrag.base import DocStatus
await rag.ainsert(
["Acme Corp opened a plant in Phoenix. CEO Jane Roe announced 500 jobs."],
ids=["news-1"], file_paths=["url-1"], track_id="wave-1",
)
counts = await rag.doc_status.get_all_status_counts()
print("processed:", counts.get("processed"), "failed:", counts.get("failed"))
processed = await rag.doc_status.get_docs_by_status(DocStatus.PROCESSED)
rows, total = await rag.doc_status.get_docs_paginated(
page=1, page_size=10, sort_field="updated_at", sort_direction="desc",
)
Duplicate documents (by filename/content hash) are recorded as
failedrather than re-extracted — so a non-zerofailedcount during bulk ingestion is the dedup pipeline working, not an error.
Loading many documents at once#
The vector storage keeps the records LightRAG hands it while a document is merged, then embeds
them in batches of embedding_batch_num and writes up to 500 rows per statement, the vectors
in binary. Inserting into an HNSW index still costs far more than building the index
afterwards. For a large one-off load, wrap the work in bulk_ingest() on each vector store:
the index is dropped for the duration and rebuilt at the end with maintenance_work_mem
raised (1GB by default). To change it, set the attribute on the store before the load, for
example
rag.chunks_vdb.maintenance_work_mem = "4GB".
documents = [
"Acme Corp opened a plant in Phoenix.",
"Jane Roe is the CEO of Acme Corp.",
]
async with rag.entities_vdb.bulk_ingest(), rag.relationships_vdb.bulk_ingest(), rag.chunks_vdb.bulk_ingest():
await rag.ainsert(documents)
Exploring the knowledge graph#
The extracted graph is a first-class property graph. Reach it through
rag.chunk_entity_relation_graph (the AgensgraphStorage instance):
g = rag.chunk_entity_relation_graph
labels = await g.get_all_labels() # every entity name
popular = await g.get_popular_labels(limit=12) # most-connected entities
print(await g.search_labels("Curie", limit=10)) # case-insensitive substring search
for name in popular:
print(name, "degree:", await g.node_degree(name))
# Export an ego-network (bounded BFS) around a hub entity.
kg = await g.get_knowledge_graph(popular[0], max_depth=2, max_nodes=40)
print(f"subgraph: {len(kg.nodes)} nodes, {len(kg.edges)} edges")
Other available methods include get_node, has_node, has_edge, get_edge,
get_node_edges, and node_degrees_batch.
LightRAG treats a relation as undirected, so the store keeps each relation once per pair of
entities, and has_edge("A", "B") and has_edge("B", "A") find the same edge. Edge reads are
answered from the edge table by graph id; has_edge and get_edge are one index probe on the
pair, so they cost the same on an entity with hundreds of relations as on one with two.
Curating the graph#
A LightRAG KG is maintainable, not write-once. These LightRAG core methods update both the graph and the vector store:
# Merge duplicate entities (fold sources into a target).
await rag.amerge_entities(source_entities=["Open AI"], target_entity="OpenAI")
# Edit an entity (re-embeds it in the vector DB).
await rag.aedit_entity("OpenAI", {"description": "OpenAI (verified).", "entity_type": "organization"})
# Edit a relation.
await rag.aedit_relation("OpenAI", "Microsoft", {"description": "partnership", "weight": 9.0})
# Delete an entity (and purge its vectors), or a whole document.
await rag.adelete_by_entity("Some Entity")
await rag.adelete_by_doc_id("doc-0")
amerge_entities accepts a merge_strategy dict (deprecated since lightrag-hku 1.5.0) that
maps a field to concatenate, keep_first, keep_last, or join_unique (for example
{"description": "concatenate", "entity_type": "keep_first"}).
Multi-tenancy with workspaces#
A workspace partitions an entire LightRAG knowledge base — all four stores — inside one
database. The graph store gives each workspace a graph of its own
(<workspace>_chunk_entity_relation, with the workspace lower-cased, other characters turned
into _, a w_ prefix if it starts with a digit or pg_, and the name cut to 63 characters),
and the other three stores keep a workspace column, so
tenants never see each other’s data. Pass it to each LightRAG:
def build(workspace: str) -> LightRAG:
return LightRAG(
working_dir=f"./rag_storage/{workspace}", workspace=workspace,
llm_model_func=gpt_4o_mini_complete, embedding_func=openai_embed,
graph_storage="AgensgraphStorage", vector_storage="AgensgraphVectorStorage",
kv_storage="AgensgraphKVStorage", doc_status_storage="AgensgraphDocStatusStorage",
)
rag_a, rag_b = build("tenant_a"), build("tenant_b")
for tenant in (rag_a, rag_b):
await tenant.initialize_storages()
print(rag_a.chunk_entity_relation_graph.graph_name) # tenant_a_chunk_entity_relation
print(rag_b.chunk_entity_relation_graph.graph_name) # tenant_b_chunk_entity_relation
# ingest into and query each one independently
for tenant in (rag_a, rag_b):
await tenant.finalize_storages()
An empty workspace, the default, uses the graph chunk_entity_relation, so a single-tenant
setup needs nothing. AGENSGRAPH_WORKSPACE also sets the workspace, and when it is set it takes
precedence over the workspace you pass to LightRAG, for every store. Leave it unset when one
process serves several tenants.
Run the demos#
The repository ships a runnable
demo suite
on real public data (Wikipedia, CC-News), each with a pre-executed notebook. Run from the
lightrag/ directory; each demo picks its own database.
Demo |
What it shows |
Scripts |
|---|---|---|
|
Build a KG; compare all five retrieval modes |
|
|
Incremental waves, doc-status, entity merging |
|
|
Top entities, search, subgraph export, multi-hop |
|
|
Merge / edit / delete entities, relations, docs |
|
|
Two isolated tenants in one database |
|
Setup: from lightrag/, uv venv .venv --python 3.13 then
uv pip install -e . -r examples/demos/requirements-demos.txt, and copy
examples/demos/.env.example to examples/demos/.env (the only .env the demos load) and fill
in your OPENAI_API_KEY. The template points at localhost:55432; change AGENSGRAPH_PORT if
your server listens elsewhere. Don’t set AGENSGRAPH_DB; each demo chooses its own. Run
01_kg_modes before 03_kg_explore, which reads its graph.
LightRAG calls the LLM on every chunk at insert, so start with a small *_LIMIT and scale up
once a demo runs end to end.