Choose where vectors live
Keep vectors in Neo4j or FalkorDB by default, or store them exclusively in Qdrant, Milvus, or Weaviate. The graph database always keeps documents, entities, relations, provenance, and full-text indexes. One vector placement covers chunks, entities, resolved entities, and communities for the lifetime of a graph.
Prepare a Qdrant server
You need a configured graph backend, the same embedder for ingestion and search, and Qdrant 1.17.0 or later. Qdrant runs as a separate service. FastEmbed computes BM25 sparse vectors locally from the exact text used for dense embedding.
-
Install the graph driver and the Qdrant extra. Replace
neo4jwithfalkordbif needed.uv pip install "agentic-graphrag[neo4j,qdrant]" -
For local development from a source checkout, start an isolated Qdrant service. The development volume persists until you explicitly remove it.
docker compose -p agrag-qdrant -f docker/docker-compose.dev.yml up -d --wait qdrantHTTP listens on
127.0.0.1:7961and gRPC on127.0.0.1:7962. OverrideQDRANT_HTTP_PORTandQDRANT_GRPC_PORTwhen another project uses those ports. Use HTTPS and authentication for a remote server. -
Set the endpoint and a unique prefix for this graph in
.env.QDRANT_URL=http://localhost:7961QDRANT_COLLECTION_PREFIX=project_notesFor an authenticated remote endpoint, also set
QDRANT_API_KEYand use anhttps://URL. The prefix must start with a letter and contain at most 41 letters, digits, or underscores. Each graph must use its own prefix. Collections are named<prefix>_chunks,<prefix>_entities,<prefix>_resolved_entities, and<prefix>_communities.
Prepare a Milvus server
Use Milvus 3.0.1 or later, or a Zilliz Cloud endpoint. Install agentic-graphrag[neo4j,milvus], replacing neo4j with falkordb if needed. In a source checkout, start an isolated local server:
docker compose -p agrag-milvus -f docker/docker-compose.dev.yml up -d --wait milvus
The service uses embedded etcd and local storage in a project-scoped volume. It publishes the SDK endpoint on 127.0.0.1:7971 and health endpoint on 127.0.0.1:7972. Override MILVUS_PORT and MILVUS_HTTP_PORT if those ports are occupied. Configure the SDK endpoint and a graph-specific prefix:
MILVUS_URI=http://localhost:7971
MILVUS_COLLECTION_PREFIX=project_notes
Non-local endpoints require HTTPS. Remote authentication also needs MILVUS_TOKEN. Set the token privately, not in the URI or Git. Construct MilvusVectorStore(settings=MilvusSettings()) from agrag.vectordb and use it in place of Qdrant in the example below. Milvus generates BM25 vectors on the server and uses normalized weighted ranking for hybrid search. Alpha zero searches BM25 only and omits records with no lexical match; alpha one searches dense vectors only. Stored BM25 text is truncated at a whole UTF-8 character within 65535 bytes without changing dense embeddings. Milvus settings lists label limits, consistency, and response-size constraints.
Prepare a Weaviate server
Install agentic-graphrag[neo4j,weaviate], replacing neo4j with falkordb if needed. Configure both HTTP and gRPC access using the Weaviate setup instructions. For cloud, use WEAVIATE_MODE=cloud, the HTTPS cluster URL, and a privately supplied API key. Keep ASYNC_INDEXING=false on custom servers.
Set WEAVIATE_COLLECTION_PREFIX to a graph-specific prefix starting with a lowercase letter. Weaviate capitalizes the first letter of collection names. The lowercase restriction prevents distinct prefixes from aliasing the same collections. Keep collection descriptions unchanged because they persist dimensions for empty collections.
Construct WeaviateVectorStore(settings=WeaviateSettings()) from agrag.vectordb and use it in place of Qdrant below. Hybrid search uses native relative-score fusion. Alpha zero uses BM25 and omits lexical nonmatches; alpha one uses dense search. Requests are capped at 10,000 results. Weaviate settings define transport and collection constraints.
Open and search the graph
Pass the Qdrant instance to both ingestion and retrieval. The example assumes graph_store, embedder, extractor, and schema are configured as in Ingest documents, and that scope is the frozenset of document keys the search may read.
from agrag.evidence import search_evidence
from agrag.ingestion import Graph
from agrag.vectordb import QdrantSettings, QdrantVectorStore
vectors = QdrantVectorStore(settings=QdrantSettings())
graph = await Graph.open(
graph_store=graph_store,
embedder=embedder,
extractor=extractor,
schema=schema,
vector_store=vectors,
)
try:
await graph.add(text="Ada worked on the Analytical Engine.")
result = await search_evidence(
graph_store,
embedder,
"Who worked on the Analytical Engine?",
scope=scope,
vector_store=vectors,
)
finally:
await vectors.close()
await graph_store.close()
Graph.open creates the four collections and checks their dimensions and distance metric on reopening. With Qdrant configured, ingestion stores no embedding property on graph nodes and creates no graph vector indexes. Each external search result is joined back to graph data before visibility and document scope are applied. An entity_search call with labels matches any requested label, including ancestor labels.
Dense scores use the graph-native scale. Hybrid search uses Qdrant's weighted reciprocal rank fusion over dense and BM25 results. RETRIEVAL_HYBRID_ALPHA defaults to 0.5; 1.0 returns dense order and graph-native dense scores, 0.0 returns BM25 order and scores while omitting nonmatches, and intermediate values return weighted RRF scores. Treat scores from the hybrid API as ranking values, not a common similarity scale across alpha settings. Resolution candidate searches remain dense. Qdrant searches are capped at 16384 results per call.
Omit vector_store everywhere to keep graph-native placement. Placement is fixed per graph. Switching between graph-native and Qdrant, changing the prefix, or changing dimensions or distance requires a separate graph and re-ingestion. VectorPlacementError reports a mismatch with recorded placement; CollectionMismatchError reports a mismatch with an existing Qdrant collection. Do not delete collections to bypass either check on a graph you intend to keep.
Repair missing vectors
A failure after a graph write can leave a committed node without a vector. The node cannot appear in vector results until its vector is restored. Reopen the graph with its original placement and run repair manually.
report = await graph.repair_vectors()
The report gives checked, missing, upserted, and pruned counts and failures per kind. Repair adds missing vectors by default. To also delete orphan vectors that have no committed graph node, use await graph.repair_vectors(prune=True). Run repair while ingestion is idle when pruning. Orphan vectors do not appear in search because the graph join drops them.
Use the same placement in MCP
Install the mcp extra too. Set MCP_VECTOR_STORE to qdrant, milvus, or weaviate, with the same backend settings used during ingestion. MCP validates placement at startup. In hybrid evidence search, the store fuses both signals internally and the reported hit has legs=["semantic"]. Lexical-only evidence search continues to use the graph full-text index.