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 to cognee.search is 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

01_search_modes

Build a KG; query it ten ways (all modes + Cypher)

build.py → ask.py

02_typed

Ontology-guided extraction (ontology_file_path)

build.py → ask.py

03_memory

Multiple datasets with node_set tags

build.py → ask.py

04_code_graph

Code graph over a repo + SearchType.CODE

build.py → ask.py

05_explore

Inspect the KG: metrics, Cypher, HTML viz

explore.py

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 search that matches nothing can raise IndexError (an upstream quirk in its history logging, not in this integration). If a query may have no hits, wrap it in a try/except IndexError returning [].