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

naive

Vector search over chunks only (no graph).

local

Entity-centric: specific entities and their facts.

global

Relationship-centric: cross-document themes.

hybrid

local + global merged.

mix

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=False in QueryParam to 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 failed rather than re-extracted — so a non-zero failed count 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

01_kg_modes

Build a KG; compare all five retrieval modes

build.py → ask.py

02_incremental

Incremental waves, doc-status, entity merging

ingest.py

03_kg_explore

Top entities, search, subgraph export, multi-hop

explore.py

04_curation

Merge / edit / delete entities, relations, docs

curate.py

05_workspace

Two isolated tenants in one database

tenants.py

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.