Usage#
This is a hands-on guide to cognee-agensgraph. It assumes you have completed
Installation — a running AgensGraph 2.17 or later with pgvector
available, and the model environment variables set before import cognee (cognee reads
them at import).
cognee is configured process-globally: you point its graph store and vector store at
an AgensGraph database, then drive the pipeline add → cognify → search.
SearchType lives at cognee.modules.search.types.SearchType.
Quickstart#
Point both stores at one AgensGraph database. The database must exist; the adapters create
the cognee graph in it, and the vector adapter enables vector if the role may.
import asyncio
import cognee
import cognee_agensgraph # registers the AgensGraph adapters
from cognee.modules.search.types import SearchType
DSN = "postgresql://localhost:55432/cognee"
# One AgensGraph database backs BOTH the knowledge graph and the embeddings.
cognee.config.set_graph_db_config({"graph_database_url": DSN, "graph_database_provider": "agensgraph"})
cognee.config.set_vector_db_config({"vector_db_url": DSN, "vector_db_provider": "agensgraph"})
Then build a memory and ask it a question. cognify and search call the models you
configured in Installation:
async def main():
await cognee.add(
[
"AgensGraph is a multi-model graph database built on PostgreSQL.",
"cognee builds a knowledge graph and embeddings from your data.",
],
dataset_name="quickstart",
)
await cognee.cognify(["quickstart"]) # chunk → LLM-extract → summarize → embed
answer = await cognee.search(
query_text="What is AgensGraph and how does cognee use it?",
query_type=SearchType.GRAPH_COMPLETION,
)
print(answer)
# Close the connection pool before the event loop ends; otherwise the program can
# hang at exit while asyncio cancels the pool's tasks.
from cognee.infrastructure.databases.graph import get_graph_engine
await (await get_graph_engine()).finalize()
asyncio.run(main())
Search modes#
cognee.search answers the same memory in many ways via query_type. Build once, then
compare modes:
question = "What is anarchism, and what ideas, people, and movements is it connected to?"
for mode in (
SearchType.GRAPH_COMPLETION, # graph-grounded answer (entities + relations + chunks)
SearchType.GRAPH_SUMMARY_COMPLETION, # the graph answer, condensed
SearchType.GRAPH_COMPLETION_COT, # graph answer with chain-of-thought
SearchType.GRAPH_COMPLETION_CONTEXT_EXTENSION, # graph answer + extra retrieved context
SearchType.RAG_COMPLETION, # plain chunk RAG, no graph (baseline)
SearchType.INSIGHTS, # entity→relation→entity triplets (no LLM)
SearchType.CHUNKS, # raw matching text chunks (vector search)
SearchType.SUMMARIES, # pre-computed per-document summaries
SearchType.NATURAL_LANGUAGE, # question → generated Cypher → graph rows
):
try:
print(mode.value, "->", await cognee.search(query_text=question, query_type=mode))
except IndexError: # cognee 0.2 raises this when a search matches nothing (see below)
print(mode.value, "->", [])
For NATURAL_LANGUAGE the model is not given the relationship names or any values: the
node labels and properties come from a fixed list in cognee’s prompt, and only the edge
property keys from the graph reach the prompt. It guesses names and values, so an open
question often gets no rows. A question that names a node and asks what it is connected to
gives it what it needs.
SearchType.CYPHER is different — you pass a Cypher query (not a question) and get graph
rows back:
rows = await cognee.search(
query_text="MATCH (n:Entity) RETURN n.name AS name LIMIT 5",
query_type=SearchType.CYPHER,
)
Each cognee class (Entity, EntityType, DocumentChunk, TextDocument, TextSummary,
…) is a vertex label of its own, and all of them are children of __node__. So
MATCH (n:Entity) reads the entities and MATCH (n:__Node__) reads every node. Label names
are lower case. Cypher folds an unquoted label to lower case, so Entity, entity and
"entity" all find it; a quoted "Entity" or "__Node__" does not. The class name as
written stays in the type property. A node whose id already exists on another label stays
on that label, so MATCH (n:Entity) can return a node whose type is EntityType. Every
relationship is an edge label named after it (contains, is_a, is_part_of, …).
GRAPH_* and RAG_COMPLETION modes return an answer (often a string); INSIGHTS returns
(source, edge, target) triplets; CHUNKS returns matching text chunks.
Typed extraction with an ontology#
Pass a domain ontology (OWL/Turtle .ttl) to cognify via ontology_file_path. Ontology
classes become EntityType nodes, and extracted entities that match an ontology individual
are flagged ontology_valid — so the graph follows your vocabulary instead of drifting.
import pathlib
ONTOLOGY = str(pathlib.Path("ontology.ttl").resolve())
await cognee.add(["...your domain documents..."], dataset_name="typed")
await cognee.cognify(["typed"], ontology_file_path=ONTOLOGY)
# Inspect alignment
from cognee.infrastructure.databases.graph import get_graph_engine
nodes, _ = await (await get_graph_engine()).get_graph_data()
vocabulary = {p.get("name") for _, p in nodes if p.get("type") == "EntityType"}
aligned = [p.get("name") for _, p in nodes if p.get("ontology_valid") and p.get("type") == "Entity"]
print("vocabulary:", vocabulary, "\naligned entities:", aligned[:15])
In the ontology file, OWL classes (:Person a owl:Class .) define the vocabulary and
individuals (:aristotle a :Person .) are known entities — an individual’s URI fragment must
match the extracted entity name.
Multi-dataset memory#
cognee is a memory layer: data lives in named datasets, built incrementally in one
database, each optionally tagged with a node_set. A single search draws on the whole
unified memory.
from cognee.infrastructure.databases.graph import get_graph_engine
from cognee.modules.engine.models.node_set import NodeSet
# Build two datasets incrementally — the graph grows, no rebuild.
await cognee.add(encyclopedia_docs, dataset_name="encyclopedia", node_set=["reference"])
await cognee.cognify(["encyclopedia"])
await cognee.add(news_docs, dataset_name="news", node_set=["current_events"])
await cognee.cognify(["news"])
# Pull back one node_set's slice of the unified memory.
g = await get_graph_engine()
for tag in ("reference", "current_events"):
ns_nodes, ns_edges = await g.get_nodeset_subgraph(NodeSet, [tag])
print(tag, len(ns_nodes), "nodes")
# One query retrieves across BOTH datasets.
await cognee.search(query_text="notable people and recent events", query_type=SearchType.CHUNKS)
Note. The
datasets=[...]argument tocognee.searchis a read-permission scope, not a retrieval filter — retrieval runs over the whole unified memory regardless. For hard per-tenant isolation, use a separate database (DSN) per tenant.
Loading a large corpus#
Inserting into an HNSW index is the main cost of a vector write. bulk_ingest() on the
vector adapter pays off on a first load: collections created inside the block get no HNSW
index until the block exits, then each is built once. Searches inside the block on those
collections read every row. With cognee 0.2, a collection that already exists keeps its
index during the block, because cognee’s indexing step creates the collection first; rows
then go into the index as usual.
from cognee.infrastructure.databases.vector import get_vector_engine
vector_engine = get_vector_engine() # the AgensgraphVectorAdapter configured above
# A first load into a fresh database, so the collections are created inside the block.
await cognee.add(corpus_docs, dataset_name="corpus")
async with vector_engine.bulk_ingest():
await cognee.cognify(["corpus"])
Building a code graph#
cognee has a dedicated pipeline that parses a Python repository into a graph of
modules/classes/functions, then searches it with SearchType.CODE. Ingest uses
run_code_graph_pipeline (an async generator), not add/cognify.
The pipeline clears everything cognee has stored first (prune_data() and
prune_system(metadata=True)), so run it against its own database. It parses Python with
tree-sitter: install tree-sitter and tree-sitter-python (both are in
examples/demos/requirements-demos.txt).
from cognee.api.v1.cognify.code_graph_pipeline import run_code_graph_pipeline
from cognee.modules.search.types import SearchType
target = "/path/to/a/python/package"
async for status in run_code_graph_pipeline(target, include_docs=False):
if "Error" in str(getattr(status, "status", "")):
raise SystemExit(getattr(status, "payload", status))
results = await cognee.search(
query_text="How does the library send an HTTP request?",
query_type=SearchType.CODE,
)
for r in results if isinstance(results, (list, tuple)) else [results]:
if isinstance(r, dict):
print(r.get("name"), "->", (r.get("content") or "")[:110])
Inspecting and visualizing the graph#
The memory is a real AgensGraph property graph. Reach it through get_graph_engine(), or
render it to a standalone HTML file.
from cognee.infrastructure.databases.graph import get_graph_engine
g = await get_graph_engine()
# Ten keys: num_nodes, num_edges, mean_degree, edge_density, num_connected_components,
# sizes_of_connected_components, and four optional ones (num_selfloops, diameter,
# avg_shortest_path_length, avg_clustering) that read -1 unless include_optional=True.
# With include_optional=True an optional value can be None, and above 5,000 nodes the
# diameter and path length are estimated and an 11th key, approximate, is True.
metrics = await g.get_graph_metrics(include_optional=False)
nodes, edges = await g.get_graph_data() # everything
disconnected = await g.get_disconnected_nodes()
entities, ent_edges = await g.get_filtered_graph_data([{"type": ["Entity"]}])
# Raw AgensGraph Cypher — top entities by degree
rows = await g.query('''
MATCH (n:Entity)
OPTIONAL MATCH (n)-[r]-()
RETURN n.id AS id, n.name AS name, count(r) AS degree ORDER BY degree DESC LIMIT 12
''')
# Traversal from the top entity, and an interactive HTML view
if rows:
neighbors = await g.get_neighbors(str(rows[0]["id"]))
connections = await g.get_connections(rows[0]["id"])
await cognee.visualize_graph("graph.html")
query() is read-only by default#
g.query(cypher) is what SearchType.CYPHER and SearchType.NATURAL_LANGUAGE run — a
statement written by a user or by a language model. It takes one statement at a time and
runs it in a read-only transaction, so a statement that writes fails. To let it write:
g = await get_graph_engine()
g.query_read_only = False # query(), CYPHER and NATURAL_LANGUAGE may now write
A read-only transaction does not stop a superuser from running a program on the database
host. The default is query_allow_server_programs = True, unlike the LangChain,
LlamaIndex and MCP paths that run generated Cypher, which refuse such a role by default. With it, a superuser role lets query(), and so CYPHER
and NATURAL_LANGUAGE, run a program on the database host through COPY … TO PROGRAM,
read-only or not. For anything but local development, connect as a role
that is not a superuser and not a member of pg_execute_server_program, and set
g.query_allow_server_programs = False; query() then refuses to run if the role can still
do this (while query_read_only is on).
query() also accepts Neo4j’s edge shorthand — (a)--(b), (a)-->(b), (a)<--(b) — which
language models write in most statements. In AgensGraph -- starts a comment, so between two
pattern nodes it is rewritten to -[]-, -[]-> and <-[]-. Everything else is sent as
written.
Resetting a memory#
To clear what cognee has built (local data plus the graph, vector, and metadata stores):
await cognee.prune.prune_data()
await cognee.prune.prune_system(metadata=True)
Run the demos#
The repository ships a runnable demo suite on real public data, each with a pre-executed notebook. Each demo creates its own database, so they never collide.
Demo |
What it shows |
Scripts |
|---|---|---|
|
Build a KG; query it ten ways (all modes + Cypher) |
|
|
Ontology-guided extraction ( |
|
|
Multiple datasets with |
|
|
Code graph over a repo + |
|
|
Inspect the KG: metrics, Cypher, HTML viz |
|
Run each script from the cognee/ directory of a repository checkout, and run
01_search_modes before 05_explore, which reads its graph. Setup, with
uv and Python 3.11 to 3.13:
uv venv .venv --python 3.13
uv pip install -e . -r examples/demos/requirements-demos.txt
cp examples/demos/.env.example examples/demos/.env # then set OPENAI_API_KEY
The *_LIMIT knobs keep a first run small (cognify calls the LLM on every chunk); the
*_RESET knobs (on by default) prune before each build.
Empty results. In cognee 0.2’s core, a
searchthat matches nothing can raiseIndexError(an upstream quirk in its history logging, not in this integration). If a query may have no hits, wrap it in atry/except IndexErrorreturning[].