Skip to main content

LlamaIndex AgensGraph

This plugin integrates AgensGraph with LlamaIndex, persisting graphs and vectors directly in AgensGraph. It powers PropertyGraphIndex and VectorStoreIndex, so you can store and query property graphs and embeddings in one database.

  • Property Graph Store: AgensPropertyGraphStore
  • Vector Store: AgensgraphVectorStore
  • Connection pool: AgensEngine (optional, shared across stores)

Demos & guides

Start here: the examples/demos/ suite — runnable, end-to-end demos on real datasets (arXiv, Wikipedia, CC-News) that show how to build with this integration at realistic scale. Its README has a quickstart and copy-paste building blocks.

Demo What you build
01 · arXiv a property graph + vector search + GraphRAG in one store, with get/get_triplets and an upsert→delete lifecycle
02 · Wikipedia an LLM-built knowledge graph + natural-language (Text2Cypher) Q&A
03 · News vector RAG: semantic, metadata-filtered (full operator set), hybrid, and cited, plus the store-mutation lifecycle
04 · Router one AgensEngine routing questions to the graph or the vector store — router and FunctionAgent

Each demo folder also ships a pre-executed notebook — a narrated, end-to-end tour with real embedded outputs.

Short, single-feature notebooks:

Requirements

  • Python 3.11+
  • AgensGraph 2.17 or later with the vector extension (for vector / HNSW search). The meta extension is used for schema introspection when present, with a catalog fallback otherwise.

Changes since 0.1

Not released yet -- this is what is in the tree.

  • AgensPropertyGraphStore.vector_query works. It hard-coded a 3-dimension cast and ordered by a fixed literal vector, so results ignored the query embedding and it errored at any other dimension. Repairing that left it emitting ORDER BY after a WITH inside a SQL sub-query, which the grammar does not allow, so every call raised until the ordering moved onto the final RETURN -- where it still reaches the HNSW index, which is the part worth checking rather than assuming.
  • Metadata-filtered vector search. Both AgensPropertyGraphStore.vector_query and AgensgraphVectorStore.query honor MetadataFilters, translated into a fully parameterized (injection-safe) Cypher WHERE. All 14 FilterOperator values are supported — EQ, NE, GT, GTE, LT, LTE, IN, NIN, CONTAINS, TEXT_MATCH, TEXT_MATCH_INSENSITIVE, ANY, ALL, IS_EMPTY — along with AND/OR/NOT conditions and nested filter groups.
  • Hybrid search. AgensgraphVectorStore(hybrid_search=True) fuses HNSW semantic search with full-text keyword search by reciprocal rank fusion — each modality is queried against its own index (so both stay index-backed) and the two rankings are merged.
  • AgensGraph-dialect Text2Cypher. The property graph store sets a default text_to_cypher_template that knows the storage model -- an element is written on the label naming what it is, and every such label inherits "__Node__" -- and avoids Neo4j-only syntax, so TextToCypherRetriever generates runnable Cypher out of the box.
  • A generated statement runs read-only. SafeTextToCypherRetriever runs what a model wrote in a transaction the server will not let write, so what it may do is the server's decision. A list of Cypher's write keywords is not that: it is PostgreSQL underneath, so INSERT, TRUNCATE, GRANT and COPY are all available and none of them is on such a list, while a read whose text merely mentions DELETE looks like a write.
  • Lazy schema introspection. AgensPropertyGraphStore(refresh_schema=False) defers the (O(N)) schema scan to the first get_schema()/get_schema_str() call, so opening a large existing graph is instant.
  • An element is written on the label naming what it is. MATCH (n:Author) reads that label's storage and nothing else, so nothing has to keep a list of types or a scalar copy of one beside every element. Measured on twenty thousand of each of two types, counting one of them: 217 buffers by label against 335 through a btree over such a copy, which also cost an index entry on every write. Each label carries its own uniqueness on id, because a constraint on the label they inherit does not reach them.
  • The embedding has a column of its own. Read out of the property map it is text in a bag that has to come out of TOAST and be parsed before a distance can be taken, once per element a filter kept -- which is where a metadata-filtered search spent its time. Over 20,000 entities with a filter keeping one in ten: 1,209 ms against 171 ms for the same complete answer -- 7x, and 111x against the un-promoted path once both are asked for every row they were asked for. The 180x first published here compared a complete answer against a partial one. Decline it with promote_embedding=False.
  • The query's mode is read. VectorStoreQuery.mode was never looked at, so every mode got a plain vector search. Hybrid, text search and MMR are answered now, alpha/sparse_top_k/hybrid_top_k do what they say, a metadata filter reaches both halves of a hybrid search, and distance_strategy takes l2 and inner_product as well as cosine.
  • Correctness fixes. Entity embeddings are persisted on upsert_nodes even when the entity has no source chunk; get(ids=[]) returns nothing (instead of the whole graph); and depth-1 get_rel_map uses a fixed pattern (AgensGraph's variable-length edges are far slower).
  • Modern vector-store node management. AgensgraphVectorStore implements get_nodes(node_ids, filters), delete_nodes(node_ids, filters) and clear() (plus async aget_nodes / adelete_nodes / aclear).
  • Richer enhanced schema. With enhanced_schema=True, numeric properties get min / max / distinct_count, list properties get min_size / max_size, and other properties get example values + distinct_count (computed exhaustively under a row threshold, sampled above it).
  • Performance. Ingest is index-backed and near-linear (a btree index on the id MERGE key; bulk add/upsert batched). Id-keyed lookups (get, get_nodes, get_triplets, get_rel_map, delete_nodes) and the vector store's delete(ref_doc_id) are index-backed rather than sequential scans, relation upserts are UNWIND-batched per type, and schema introspection no longer materializes every distinct property value. Metadata-filter keys can be indexed with create_property_index(...). See Performance & indexing.
  • True async. All twelve async methods the contract declares are implemented here. The base class answers most of them by calling the synchronous one, which holds the event loop for the whole round trip: twelve concurrent rel maps ran in 76.9 ms with the loop doing nothing else at all, against 31.8 ms with it still running other work. See Async & connection pooling.
  • Nothing is installed in your database. Opening a store used to create three plpgsql functions there. The catalogs answer the same questions, and faster: on twenty thousand elements carrying embeddings, 1,213 ms of walking every one of them against 19 ms.
  • Breaking change. The deprecated triplet AgensGraphStore (Knowledge Graph Store) has been removed. Use AgensPropertyGraphStore with PropertyGraphIndex.

Installation

pip install llama-index llama-index-agensgraph

Usage

Property Graph Store

import os
import urllib.request
import nest_asyncio
from llama_index.core import SimpleDirectoryReader, PropertyGraphIndex
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.core.indices.property_graph import SchemaLLMPathExtractor

from llama_index_agensgraph.graph_stores.agensgraph import AgensPropertyGraphStore

os.environ[
    "OPENAI_API_KEY"
] = "<YOUR_API_KEY>"  # Replace with your OpenAI API key

url = (
    "https://raw.githubusercontent.com/run-llama/llama_index/main/docs/"
    "examples/data/paul_graham/paul_graham_essay.txt"
)
output_path = "data/paul_graham/paul_graham_essay.txt"
os.makedirs("data/paul_graham/", exist_ok=True)
urllib.request.urlretrieve(url, output_path)

nest_asyncio.apply()

# Nothing to escape: every value reaches the server as a bound parameter, so an
# apostrophe in the text is just an apostrophe. This used to replace them, which
# put backslashes into the reader's own documents.
documents = SimpleDirectoryReader("./data/paul_graham/").load_data()

# Setup AgensGraph connection (ensure AgensGraph is running)
conf = {
    "dbname": "",
    "user": "",
    "password": "",
    "host": "",
    "port": 5432,
}

# Pass vector_dimension to enable the HNSW vector index (match your embedding
# model's dimension, e.g. 1536 for text-embedding-3-small). Without it, vector
# search still works but is unindexed.
graph_store = AgensPropertyGraphStore(
    graph_name="graph",
    conf=conf,
    vector_dimension=1536,
)

index = PropertyGraphIndex.from_documents(
    documents,
    embed_model=OpenAIEmbedding(model_name="text-embedding-3-small"),
    kg_extractors=[
        SchemaLLMPathExtractor(
            llm=OpenAI(model="gpt-4o-mini", temperature=0.0),
            # strict=True can yield zero triplets with some models; strict=False is more forgiving
            strict=False,
        )
    ],
    property_graph_store=graph_store,
    show_progress=True,
)

query_engine = index.as_query_engine(include_text=True)

response = query_engine.query("What happened at Interleaf and Viaweb?")
print("\nDetailed Query Response:")
print(str(response))

Natural-language queries (Text2Cypher)

TextToCypherRetriever turns a question into AgensGraph Cypher using the store's built-in dialect prompt — no custom prompt needed:

from llama_index.core.indices.property_graph import TextToCypherRetriever

retriever = TextToCypherRetriever(
    graph_store=graph_store, llm=OpenAI(model="gpt-4o-mini")
)
nodes = retriever.retrieve("How many entities of each type are there?")
print(nodes[0].node.text)  # the generated Cypher and its result

Vector Store

import os
import urllib.request
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext
from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore

# Set your OpenAI API key
os.environ["OPENAI_API_KEY"] = "<YOUR_API_KEY>"  # Replace with your key

# Download example data
os.makedirs("data/paul_graham/", exist_ok=True)
url = (
    "https://raw.githubusercontent.com/run-llama/llama_index/main/docs/"
    "examples/data/paul_graham/paul_graham_essay.txt"
)
output_path = "data/paul_graham/paul_graham_essay.txt"
urllib.request.urlretrieve(url, output_path)

# Load documents
documents = SimpleDirectoryReader("./data/paul_graham").load_data()

# Setup AgensGraph connection (ensure AgensGraph is running)
url = "postgresql://username:password@host:port/database_name"
embed_dim = 1536

# Initialize vector store
vector_store = AgensgraphVectorStore(url=url, embedding_dimension=embed_dim)

# Build index
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(documents, storage_context=storage_context)

# Query
query_engine = index.as_query_engine()
response = query_engine.query("What happened at Interleaf?")
print("\nQuery Response:")
print(str(response))

For hybrid (vector + keyword) search, build the store with hybrid_search=True and query with a query_str:

hybrid_store = AgensgraphVectorStore(
    url=url, embedding_dimension=embed_dim, hybrid_search=True
)
index = VectorStoreIndex.from_vector_store(hybrid_store)
index.as_retriever(vector_store_query_mode="hybrid").retrieve(
    "What happened at Interleaf?"
)

Async & connection pooling

By default each store opens a single dedicated connection. For concurrent workloads, share an AgensEngine (a psycopg connection pool) across stores so each request checks out its own connection instead of serializing on one:

from llama_index_agensgraph.engine import AgensEngine
from llama_index_agensgraph.graph_stores.agensgraph import AgensPropertyGraphStore
from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore

engine = AgensEngine.from_url(
    "postgresql://user:pwd@host:5432/db", min_size=2, max_size=20
)

graph_store = AgensPropertyGraphStore(graph_name="graph", conf=conf, engine=engine)
vector_store = AgensgraphVectorStore(
    url="postgresql://user:pwd@host:5432/db",
    embedding_dimension=1536,
    engine=engine,
)

# ... use the stores ...
engine.close()  # await engine.aclose() if you used the async pool

The stores also provide true-async hot paths backed by psycopg.AsyncConnection (no thread-pool wrapping):

  • Vector store: async_add, aquery, adelete
  • Property graph store: aupsert_nodes, aupsert_relations, aget, avector_query, astructured_query

These work with or without an AgensEngine; with one, they draw from the async pool.

Performance & indexing

The stores are indexed for their hot paths out of the box:

  • Ingest (MERGE-by-id) is backed by a btree index on id, so bulk upsert/add stays near-linear rather than O(N²).
  • Vector search uses the HNSW index on the embedding.
  • Lookups by id (get / get_nodes / delete_nodes) and the vector store's delete(ref_doc_id) are index-backed.
  • Relation upserts are UNWIND-batched per relationship type (not one query per relation).

Metadata-filtered vector search. A metadata filter cannot use the HNSW index for the filter itself, so a filter on an un-indexed property degrades to a sequential scan over the embedded nodes. Index the keys you filter on to keep it fast:

# Property graph store
graph_store.create_property_index("country")

# Vector store
vector_store.create_property_index("topic")

With the index present, the planner preselects matching rows via an index/bitmap scan and then ranks them — instead of scanning every node.

Counting and type filters. Prefer count(*) over count(n) in aggregations: count(n) materializes each matched node (including its embedding), so it is much slower on a graph that stores embeddings. For "all nodes of type X", match the label itself with MATCH (n:X), which reads only that label's storage.

Release files for llama-index-agensgraph 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for llama-index-agensgraph 0.3.0
File Size Uploaded
llama_index_agensgraph-0.3.0.tar.gz 92.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for llama-index-agensgraph 0.3.0
File Interpreter ABI Platform
llama_index_agensgraph-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 162.5 kB

Release files / llama_index_agensgraph-0.3.0.tar.gz

Download URL llama_index_agensgraph-0.3.0.tar.gz
Size 92.8 kB
Tags Source
SHA-256 checksum
How to use checksums
fa7ff40c567ee7b891fa6824e10a12a70f42b7d22f47500b9e01743d1d034093
BLAKE2b-256 checksum
How to use checksums
0200967f86449ebf28a4d519fed01141d58917fe3f693055060c7c72d39f6889
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / llama_index_agensgraph-0.3.0-py3-none-any.whl

Download URL llama_index_agensgraph-0.3.0-py3-none-any.whl
Size 69.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
90671f6570890a638a86c8b8a233f6dc0ea9cfa4e1fee7550cf614e245d6e374
BLAKE2b-256 checksum
How to use checksums
6cc16af843b968234d6630abe44087234599497bd19a603c581961738431700d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release history Release notifications | RSS feed

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page