Skip to main content

Retrieve and answer

This guide shows how to use SearchEngine to retrieve entities and chunks from a knowledge graph. It also shows how to use build_agent to get cited answers.

Prerequisites​

Install the storage, embedding, and extraction extras. The agent layer needs the agents extra:

pip install 'agentic-graphrag[neo4j,extract]'
# For the agent layer:
pip install 'agentic-graphrag[agents]'

Each code block on this page is complete. It opens a graph with the GENERIC schema, adds two sentences, and closes the graph at the end. The agent examples read LLM_BASE_URL, LLM_MODEL_ID, and LLM_API_KEY from the environment or from .env.

Direct SearchEngine Usage​

SearchEngine searches entity labels that exist in your graph. The example below ingests its data with the GENERIC schema, so GENERIC labels match. If you ingested with another schema, pass that schema's labels in entity_labels.

import asyncio

from agrag.retrieval.recipes import HYBRID
from agrag.retrieval.search_engine import SearchEngine
from agrag.common.data_models.graph_schema import GENERIC
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import Graph
from agrag.ingestion.extract import GlinerExtractor


async def main() -> None:
graph_store = build_graph_store("neo4j")
embedder = build_embedder("ibm-granite/granite-embedding-small-english-r2")
graph = await Graph.open(
schema=GENERIC,
graph_store=graph_store,
embedder=embedder,
extractor=GlinerExtractor(),
)
try:
await graph.add(text="Aspirin treats headaches. Ibuprofen also treats headaches.")
engine = SearchEngine(
graph_store=graph_store,
embedder=embedder,
entity_labels=[entity.label for entity in GENERIC.entities],
)

results = await engine.search("What treats headaches?", HYBRID)

for result in results:
print(f"{result.item} (score={result.score:.3f})")
finally:
await graph_store.close()


asyncio.run(main())

Child chunks and their parents​

When you chunk with ParentChildChunker, a chunk search returns child chunks. Each SearchResult has the parent chunk in SearchResult.parent. It is None for every other result. The agent sees the parent text under the citation key of the first child hit, for example [C1]. A later child of the same parent shows its own text and the note (part of [C1]). A citation always names the child. See Configure chunking for the setup.

Using build_agent​

import asyncio

from agrag.agents.build import build_agent
from agrag.agents.settings import AgentLLMSettings
from agrag.retrieval.search_engine import SearchEngine
from agrag.common.data_models.graph_schema import GENERIC
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import Graph
from agrag.ingestion.extract import GlinerExtractor


async def main() -> None:
graph_store = build_graph_store("neo4j")
embedder = build_embedder("ibm-granite/granite-embedding-small-english-r2")
graph = await Graph.open(
schema=GENERIC,
graph_store=graph_store,
embedder=embedder,
extractor=GlinerExtractor(),
)
try:
await graph.add(text="Aspirin treats headaches. Ibuprofen also treats headaches.")
engine = SearchEngine(graph_store=graph_store, embedder=embedder)

agent = build_agent(
engine=engine,
llm_settings=AgentLLMSettings.from_openai_compatible_env(),
graph_schema=GENERIC,
)

result = await agent.ainvoke({
"messages": [
{"role": "user", "content": "What treats headaches?"}
]
})

last_message = result["messages"][-1]
print(last_message["content"] if isinstance(last_message, dict) else last_message.content)
finally:
await graph_store.close()


asyncio.run(main())

result["ledger"] holds the citation keys the agent assigned during the run. Call result["ledger"].resolve("C1") to get the SearchResult behind a key in the answer. It returns None for a key the run never assigned. Each call to ainvoke returns a new ledger.

The deep-agent path returns LangChain message objects, so the last message's text is .content. The optional-dependency fallback returns dictionaries, as shown above. graph_schema tells the researcher's prompt and the generated-query tool which labels and relationship types the graph uses. Omit it to use the engine's own schema, which falls back to GENERIC when the engine was built without one. If you pass a schema that differs from the engine's schema, build_agent raises ValueError. The prompt can never describe a graph that the engine does not search.

Scoping agent retrieval​

By default, the agent's tools search the whole graph. Pass SearchFilters to limit every tool search by document set, tenant, or label:

import asyncio

from agrag.agents.build import build_agent
from agrag.agents.settings import AgentLLMSettings
from agrag.retrieval.filters import SearchFilters
from agrag.retrieval.search_engine import SearchEngine
from agrag.common.data_models.graph_schema import GENERIC
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import Graph
from agrag.ingestion.extract import GlinerExtractor


async def main() -> None:
graph_store = build_graph_store("neo4j")
embedder = build_embedder("ibm-granite/granite-embedding-small-english-r2")
graph = await Graph.open(
schema=GENERIC,
graph_store=graph_store,
embedder=embedder,
extractor=GlinerExtractor(),
)
try:
await graph.add(text="Aspirin treats headaches. Ibuprofen also treats headaches.")
engine = SearchEngine(graph_store=graph_store, embedder=embedder)

agent = build_agent(
engine=engine,
llm_settings=AgentLLMSettings.from_openai_compatible_env(),
filters=SearchFilters(document_ids=["doc-1", "doc-2"]),
)
finally:
await graph_store.close()


asyncio.run(main())

Set the filters when your application builds the agent. The tools enforce the filters. The model cannot see or change them.

Build-time filters and per-call tool parameters have different roles. The caller sets filters, and the model cannot see them. The model chooses the tool parameters for each call, such as labels, document_ids, limit, min_score, relation_type, and direction. These parameters can only narrow the build-time scope. They cannot widen it. The tools refuse requests outside the scope.

The document_ids filter applies to the five discovery tools and entity lookup. It also limits neighbors from find_related_entities and traverse_from_entity. The labels and properties filters apply to entity lookup and traversal neighbors. relation_types is a traversal allowlist. list_relationship_types uses only this allowlist. The eleventh tool, query_graph_directly, is outside both groups. Unscoped agents receive it. Caller-scoped agents do not.

Researcher tools​

The researcher subagent receives eleven tools when the agent is unscoped, ten when caller-set base filters are non-empty. The tools fall into three groups plus a calculator.

Discovery tools find entry points into the graph:

ToolPurposeKey parameters
search_source_textFind passages of source text that mention the query termsquery, limit, document_ids
look_up_entityFind entities that match a name or descriptionquery, limit, labels
explore_relatedSearch entities and source text together for a topicquery, limit, labels, document_ids
answer_from_graph_structureAnswer from reranked entity and passage evidencequery, limit, min_score
answer_thematic_questionAnswer a broad question from community summariesquery, limit

Structural navigation tools read what the graph stores about one entity:

ToolPurposeKey parameters
describe_entityShow an entity's own recorded properties, not its neighborsentity
list_relationship_typesList the relationship types attached to an entityentity, relation_type_filter

Relational tools follow relationships between entities:

ToolPurposeKey parameters
find_related_entitiesFind the entities one relationship type away from an entityentity, relation_type, direction, community_expand
traverse_from_entityWalk outward from an entity, with a fallback to relationship types when the neighborhood is too wideentity, relation_type, direction, depth
query_graph_directlyAsk a structural question in one generated read-only Cypher query; present only for unscoped agentsquery

If the generated query fails, query_graph_directly returns an error message, not "No results found.", so the agent can try another tool.

compute_over_evidence stands outside the three groups. It computes a count, a sum, or a comparison over numbers the researcher already read from earlier tool results. It never queries the graph.

Available Recipes​

RecipeMethodsBFSReranker
ENTITYentityNoNone
CHUNKchunkNoNone
HYBRIDentity, chunkNoNone
HYBRID_RERANKEDentity, chunkNocross_encoder
GRAPH_EXPANDentityYesNone
THEMATICcommunityNoNone
TEXT2CYPHERtext2cypherNoNone

Configuration​

Set environment variables with the RETRIEVAL_ prefix:

RETRIEVAL_ENTITY_LABELS='["Drug","Condition"]'
RETRIEVAL_ENTITY_TOP_K=10
RETRIEVAL_CHUNK_TOP_K=10
RETRIEVAL_HYBRID_ALPHA=0.5
RETRIEVAL_TRAVERSAL_DEPTH=2

RETRIEVAL_ENTITY_LABELS (or the entity_labels argument) names the schema entity labels that entity search runs against. Ingestion creates one native vector index per label, so search needs the labels themselves. The RETRIEVAL_ENTITY_COLLECTION name does not apply here. It applies only when you configure a separate VectorStore.