# Usage This is a hands-on guide to `llama-index-agensgraph` 0.3.1. It assumes you have completed [Installation](installation.md) — a running AgensGraph 2.17 or later with the `pgvector` extension. The examples that call a model also need `OPENAI_API_KEY`; the ones that pass in embeddings by hand do not. The integration plugs into LlamaIndex's standard indices. Import paths: ```python from llama_index_agensgraph.graph_stores.agensgraph import AgensPropertyGraphStore from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore from llama_index_agensgraph.retrievers import SafeTextToCypherRetriever from llama_index_agensgraph.engine import AgensEngine ``` The property graph store writes each node on a label named after its type — a `Person` entity on the vertex label `"Person"`, a text chunk on `"Chunk"` — and every one of those labels inherits from `"__Node__"`. `MATCH (n:"Person")` reads only that label's storage, `MATCH (n:"__Node__")` still reaches every node, and `label(n)` returns a node's type. The embedding is kept in a column of its own rather than in the property map, when the store is given a `vector_dimension` and the server can give a property a column. The store's Text2Cypher prompt describes this layout, so generated Cypher matches it. Every statement goes through the [`agensgraph-python`](https://pypi.org/project/agensgraph-python/) 2.0 driver. Both stores implement their async methods (`aquery`, `async_add`, `aupsert_nodes`, `avector_query`, `astructured_query`, …) on the driver's async connection instead of running the synchronous ones on the event loop. ## Quickstart The simplest "it works": a `VectorStoreIndex` on `AgensgraphVectorStore`. The store creates the graph, the HNSW index, and `CREATE EXTENSION IF NOT EXISTS vector` on construction. ```python import os from llama_index.core import VectorStoreIndex, StorageContext from llama_index.core.schema import TextNode from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore os.environ["OPENAI_API_KEY"] = "sk-..." url = "postgresql://app:app@localhost:5432/mydb" vector_store = AgensgraphVectorStore(url=url, embedding_dimension=1536) # match your model nodes = [ TextNode(text="The Shawshank Redemption", metadata={"theme": "Friendship", "year": 1994}), TextNode(text="Inception", metadata={"theme": "Fiction", "year": 2010}), TextNode(text="1984", metadata={"theme": "Totalitarianism", "year": 1949}), ] ``` Index the nodes and ask a question. `VectorStoreIndex` embeds each node with the default OpenAI embedding model and writes it to the store: ```python index = VectorStoreIndex(nodes, storage_context=StorageContext.from_defaults(vector_store=vector_store)) print(index.as_query_engine().query("What is a movie about dreams?")) ``` `url` and `embedding_dimension` are required; `embedding_dimension` must match your model (1536 for `text-embedding-3-small`). ## Property graph index Let an LLM extract a typed knowledge graph from documents with `SchemaLLMPathExtractor`, store it in AgensGraph, and query it. ```python from typing import Literal from llama_index.core import Document, PropertyGraphIndex from llama_index.core.indices.property_graph import SchemaLLMPathExtractor from llama_index.embeddings.openai import OpenAIEmbedding from llama_index.llms.openai import OpenAI from llama_index_agensgraph.graph_stores.agensgraph import AgensPropertyGraphStore conf = {"dbname": "mydb", "user": "app", "password": "app", "host": "localhost", "port": 5432} graph_store = AgensPropertyGraphStore( graph_name="li_kg", conf=conf, vector_dimension=1536, # builds the HNSW entity index; omit to skip it (slower search) enhanced_schema=True, # richer property stats for Text2Cypher create=True, create_indexes=True, refresh_schema=True, ) documents = [ Document(text="Ada Lovelace, born in London, worked with Charles Babbage on the Analytical Engine."), Document(text="Babbage founded the Analytical Society in Cambridge."), ] ``` Then extract the graph with a schema you choose, and query it: ```python llm = OpenAI(model="gpt-4o-mini", temperature=0.0) extractor = SchemaLLMPathExtractor( llm=llm, possible_entities=Literal["Person", "Organization", "Place", "Work"], possible_relations=Literal["FOUNDED", "LOCATED_IN", "BORN_IN", "CREATED", "MEMBER_OF"], strict=False, # see the note below max_triplets_per_chunk=10, ) index = PropertyGraphIndex.from_documents( documents, property_graph_store=graph_store, embed_model=OpenAIEmbedding(model_name="text-embedding-3-small"), llm=llm, kg_extractors=[extractor], embed_kg_nodes=True, show_progress=True, ) print(index.as_query_engine(include_text=True).query("Who worked on the Analytical Engine?")) ``` `SchemaLLMPathExtractor` upper-cases the types it is given, so this graph holds `"PERSON"`, `"WORK"` and `"ORGANIZATION"` labels: match `(p:"PERSON")` on it, not `(p:"Person")`. > **`strict=False`.** With `strict=True`, `gpt-4o-mini` often returns zero triplets when the > schema has many types; `strict=False` still steers the model with your ontology but > reliably produces a graph. To attach to an already-populated graph without re-extracting: ```python index = PropertyGraphIndex.from_existing( property_graph_store=graph_store, embed_model=OpenAIEmbedding(model_name="text-embedding-3-small"), llm=llm, kg_extractors=[], ) ``` ## Working with the graph store directly `PropertyGraphIndex` drives the store through LlamaIndex's `PropertyGraphStore` interface, and you can call the same methods yourself — to load entities you already have, or to look at what an extractor wrote. The embeddings below are three numbers long to keep the example short; in practice they come from your embedding model, and `vector_dimension` matches it. ```python from llama_index.core.graph_stores.types import EntityNode, Relation from llama_index_agensgraph.graph_stores.agensgraph import AgensPropertyGraphStore conf = {"dbname": "mydb", "user": "app", "password": "app", "host": "localhost", "port": 5432} store = AgensPropertyGraphStore(graph_name="li_people", conf=conf, vector_dimension=3) ada = EntityNode(name="Ada Lovelace", label="Person", properties={"born": 1815}, embedding=[1.0, 0.0, 0.0]) babbage = EntityNode(name="Charles Babbage", label="Person", embedding=[0.9, 0.1, 0.0]) analytical = EntityNode(name="Analytical Engine", label="Work", embedding=[0.0, 1.0, 0.0]) store.upsert_nodes([ada, babbage, analytical]) store.upsert_relations([ Relation(label="WORKED_ON", source_id=ada.id, target_id=analytical.id), Relation(label="CREATED", source_id=babbage.id, target_id=analytical.id), ]) print(store.get(ids=[ada.id])) print(store.get_triplets(entity_names=["Ada Lovelace"])) print(store.structured_query( 'MATCH (p:"Person")-[r]->(w:"Work") RETURN p.name AS who, type(r) AS rel, w.name AS what' )) print(store.get_schema_str(refresh=True)) ``` Each type is written on its own label, so counting by type is a match on the labels: ```python print(store.structured_query('MATCH (n:"__Node__") RETURN label(n) AS type, count(*) AS n')) ``` Prefer `count(*)` over `count(n)`: `count(n)` materializes each matched node, embedding included. `vector_query` ranks entities by their embedding, and honors `MetadataFilters` (see [Metadata-filtered search](#metadata-filtered-search)): ```python from llama_index.core.vector_stores.types import VectorStoreQuery nodes, scores = store.vector_query( VectorStoreQuery(query_embedding=[1.0, 0.05, 0.0], similarity_top_k=2) ) print([n.name for n in nodes], scores) ``` To load a large corpus, wrap the writes in `bulk_ingest()`. It drops the vector indexes for the duration of the block, then builds them again and collects statistics when the block ends. A search inside the block reads every element — for every user of the graph, not only this one — so it is for loading, not for a store that is answering questions. ```python with store.bulk_ingest(): store.upsert_nodes([ EntityNode(name=f"Author {i}", label="Person", embedding=[0.1 * i, 0.2, 0.3]) for i in range(100) ]) ``` A filtered search cannot use the HNSW index for the filter itself, so index the keys you filter on with `store.create_property_index("country")`. The index is created on every label that holds elements, and on labels written later. ## Text2Cypher retrieval `TextToCypherRetriever` turns a question into AgensGraph Cypher using the store's built-in dialect prompt — no custom prompt needed. `strip_markdown` takes the statement out of the code fence a model tends to wrap it in: ```python from llama_index.core.indices.property_graph import TextToCypherRetriever from llama_index.llms.openai import OpenAI from llama_index_agensgraph.retrievers import strip_markdown retriever = TextToCypherRetriever( graph_store=graph_store, llm=OpenAI(model="gpt-4o-mini"), cypher_validator=strip_markdown, # a fenced answer is otherwise a syntax error ) nodes = retriever.retrieve("How many entities of each type are there?") print(nodes[0].node.text) # the generated Cypher and its result ``` `TextToCypherRetriever` runs whatever the model wrote, with nothing stopping a write. For anything beyond a trusted notebook, use `SafeTextToCypherRetriever`. It runs the generated statement in a read-only transaction, so the server refuses a write however it is spelled, and it holds the text to a single statement. When the statement fails, it logs a warning and returns no nodes instead of aborting the whole query engine: ```python from llama_index.llms.openai import OpenAI from llama_index_agensgraph.retrievers import SafeTextToCypherRetriever, strip_markdown retriever = SafeTextToCypherRetriever( graph_store=graph_store, llm=OpenAI(model="gpt-4o-mini"), cypher_validator=strip_markdown, ) nodes = retriever.retrieve("Which people worked on the Analytical Engine?") ``` > **Connect as an ordinary role.** A read-only transaction does not stop `COPY ... TO PROGRAM`, > so a role that may run a command on the server's host — a superuser, or a member of > `pg_execute_server_program` — is refused with a `ConfigurationError`. Connect the store as a > role that holds neither, like the `app` role in [Installation](installation.md#connecting). > To accept such a role anyway, say so explicitly with > `SafeTextToCypherRetriever(..., allow_server_programs=True)`, or > `store.read_only(allow_server_programs=True)` on the store. The repository's demos do, > because they connect as a superuser to a database they own. The same read-only block is available on the store, for any Cypher you did not write yourself: ```python from llama_index_agensgraph.graph_stores.agensgraph.utils import AgensQueryException with store.read_only(): print(store.structured_query('MATCH (p:"Person") RETURN count(*) AS n')) try: store.structured_query("""CREATE (:"Person" {name: 'Mallory'})""") except AgensQueryException as e: print(e.details) # 25006: cannot write to a graph in a read-only transaction ``` ## Vector store Build a `VectorStoreIndex` over documents, or reopen an existing store without re-ingesting: ```python from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore vector_store = AgensgraphVectorStore(url=url, embedding_dimension=1536) documents = SimpleDirectoryReader("./data").load_data() index = VectorStoreIndex.from_documents( documents, storage_context=StorageContext.from_defaults(vector_store=vector_store) ) print(index.as_query_engine().query("What happened at Interleaf?")) # Reopen later without re-embedding: index = VectorStoreIndex.from_vector_store(AgensgraphVectorStore(url=url, embedding_dimension=1536)) ``` Constructor knobs (defaults): `graph_name="vector_store"`, `index_name="vector"`, `keyword_index_name="keyword"`, `node_label="Chunk"`, `embedding_node_property="embedding"`, `text_node_property="text"`, `distance_strategy="cosine"` (or `"l2"`, its alias `"euclidean"`, and `"inner_product"`), `hybrid_search=False`, `promote_embedding=True`, `engine=None`. `promote_embedding` keeps the embedding in a column of its own; `False` leaves it in the property map. The store can also be used without an index, with embeddings you computed yourself. The next two sections use it this way, with three-number embeddings: ```python from llama_index.core.schema import TextNode from llama_index.core.vector_stores.types import VectorStoreQuery from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore url = "postgresql://app:app@localhost:5432/mydb" movies = AgensgraphVectorStore(url=url, embedding_dimension=3, graph_name="li_movies", hybrid_search=True) movies.add([ TextNode(id_="shawshank", text="The Shawshank Redemption, a story of friendship in prison", metadata={"theme": "Friendship", "year": 1994}, embedding=[1.0, 0.0, 0.0]), TextNode(id_="inception", text="Inception, a thief who steals secrets through dreams", metadata={"theme": "Fiction", "year": 2010}, embedding=[0.0, 1.0, 0.0]), TextNode(id_="1984", text="1984, a novel of surveillance and totalitarian rule", metadata={"theme": "Totalitarianism", "year": 1949}, embedding=[0.0, 0.0, 1.0]), ]) result = movies.query(VectorStoreQuery(query_embedding=[0.1, 0.9, 0.0], similarity_top_k=2)) print([n.node_id for n in result.nodes], result.similarities) ``` `bulk_ingest()` works here as it does on the graph store: `with movies.bulk_ingest():` around the `add` calls drops the vector index for the block and builds it after. ## Metadata-filtered search Scope semantic search with LlamaIndex's standard `MetadataFilters`, translated to parameterized Cypher. Through an index, pass them as `index.as_retriever(filters=filters)`; on the store, put them on the `VectorStoreQuery`: ```python from llama_index.core.vector_stores import ( MetadataFilter, MetadataFilters, FilterOperator, FilterCondition, ) filters = MetadataFilters( filters=[ MetadataFilter(key="theme", value="Fiction"), # EQ is the default MetadataFilter(key="year", value=1997, operator=FilterOperator.GT), ], condition=FilterCondition.AND, # or FilterCondition.OR ) result = movies.query( VectorStoreQuery(query_embedding=[0.1, 0.9, 0.0], similarity_top_k=2, filters=filters) ) print([n.node_id for n in result.nodes]) # ['inception'] ``` All 14 operators are supported — `EQ, NE, GT, GTE, LT, LTE, IN, NIN, CONTAINS, TEXT_MATCH, TEXT_MATCH_INSENSITIVE, ANY, ALL, IS_EMPTY` — plus `AND`/`OR`/`NOT` and nested filter groups. A filtered predicate can't use the HNSW index, so index the keys you filter on: ```python movies.create_property_index("theme") ``` The same `MetadataFilters` objects also drive `get_nodes(filters=...)` and `delete_nodes(filters=...)`: ```python old = MetadataFilters(filters=[MetadataFilter(key="year", value=1990, operator=FilterOperator.LT)]) print([n.node_id for n in movies.get_nodes(filters=old)]) # ['1984'] ``` ## Hybrid search and other query modes The store answers the mode a query asks for: `default` (vector search), `hybrid` (and `semantic_hybrid`), `text_search` (and `sparse`), and `mmr`. Hybrid search needs a store built with `hybrid_search=True`, which adds a full-text index; asking a store built without one raises `ValueError`. Text search runs on any store, but without that index it reads every row. Hybrid search queries each modality against its own index (HNSW and full-text) and fuses the two rankings by reciprocal rank fusion. `alpha` weighs the vector side: `1.0` is the vector search alone, `0.0` the text search alone, and the default is `0.5`. `sparse_top_k` sets how many candidates the text side returns, and `hybrid_top_k` how many fused results come back. A metadata filter applies to both halves. ```python from llama_index.core.vector_stores.types import VectorStoreQueryMode hybrid = movies.query(VectorStoreQuery( query_embedding=[0.1, 0.9, 0.0], query_str="dreams", similarity_top_k=2, mode=VectorStoreQueryMode.HYBRID, alpha=0.5, )) text = movies.query(VectorStoreQuery( query_str="friendship", similarity_top_k=2, mode=VectorStoreQueryMode.TEXT_SEARCH, )) diverse = movies.query(VectorStoreQuery( query_embedding=[0.1, 0.9, 0.0], similarity_top_k=2, mode=VectorStoreQueryMode.MMR, mmr_threshold=0.5, )) print([n.node_id for n in hybrid.nodes], [n.node_id for n in text.nodes], [n.node_id for n in diverse.nodes]) ``` Through an index, the same options are retriever arguments: `index.as_retriever(similarity_top_k=5, vector_store_query_mode="hybrid", alpha=0.5)`. ## Connection pooling and async By default each store runs its synchronous methods on one connection of its own, and its async methods open a connection per call (up to 10 at once) and close it afterwards. For concurrent workloads, share an `AgensEngine` — a driver connection pool, sync and async — across stores, so each request borrows a connection that is already open. The async methods draw from the engine's async pool: ```python from llama_index.core.schema import TextNode from llama_index.core.vector_stores.types import VectorStoreQuery from llama_index_agensgraph.engine import AgensEngine from llama_index_agensgraph.vector_stores.agensgraph import AgensgraphVectorStore url = "postgresql://app:app@localhost:5432/mydb" pool = AgensEngine.from_url(url, min_size=2, max_size=20) # or .from_conf(conf, ...) notes = AgensgraphVectorStore(url=url, embedding_dimension=3, graph_name="li_notes", engine=pool) await notes.async_add([TextNode(id_="n1", text="pooled", embedding=[1.0, 0.0, 0.0])]) found = await notes.aquery(VectorStoreQuery(query_embedding=[1.0, 0.0, 0.0], similarity_top_k=1)) print([n.node_id for n in found.nodes]) await notes.aclose() await pool.aclose() # pool.close() when only the sync pool was used ``` One engine can serve a graph store and vector stores on different graphs, which a `RouterQueryEngine` can then route between. The graphs here are the demos': `arxiv` is built by `01_arxiv_pg` (the store opens it with `create=False`, so it must exist) and `news` by `03_news_vector_rag`. Both are in the demos' database, `llamaindex_demos` by default, so point `url` and `conf` at it: ```python from llama_index.core import PropertyGraphIndex, VectorStoreIndex from llama_index.core.query_engine import RouterQueryEngine from llama_index.core.selectors import LLMSingleSelector from llama_index.core.tools import QueryEngineTool from llama_index.embeddings.openai import OpenAIEmbedding from llama_index.llms.openai import OpenAI 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 llm, embed = OpenAI(model="gpt-4o-mini"), OpenAIEmbedding(model_name="text-embedding-3-small") engine = AgensEngine.from_url(url, min_size=2, max_size=20) graph_qe = PropertyGraphIndex.from_existing( property_graph_store=AgensPropertyGraphStore("arxiv", conf=conf, vector_dimension=1536, create=False, engine=engine), embed_model=embed, llm=llm, kg_extractors=[], ).as_query_engine() news_qe = VectorStoreIndex.from_vector_store( AgensgraphVectorStore(url=url, embedding_dimension=1536, graph_name="news", node_label="Article", engine=engine), embed_model=embed, ).as_query_engine(similarity_top_k=5) router = RouterQueryEngine.from_defaults( query_engine_tools=[ QueryEngineTool.from_defaults(graph_qe, name="papers", description="research papers and authors"), QueryEngineTool.from_defaults(news_qe, name="news", description="current events"), ], selector=LLMSingleSelector.from_defaults(llm=llm), llm=llm, ) try: print(router.query("What are companies doing with AI?")) finally: engine.close() # await engine.aclose() after async use ``` ## Run the demos The repository ships a runnable [demo suite](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/demos), each with a pre-executed notebook. | Demo | What it shows | Scripts | | ---- | ------------- | ------- | | `01_arxiv_pg` | Property graph + vector search + GraphRAG over arXiv | `prepare.py` → `query.py` | | `02_wikipedia_pgindex` | LLM-extracted KG + full retriever stack + Text2Cypher | `build.py` → `ask.py` | | `03_news_vector_rag` | Semantic, metadata-filtered, hybrid, and cited RAG | `ingest.py` → `rag.py` | | `04_router` | One `AgensEngine` routing graph-vs-vector, as a router and a `FunctionAgent` | `router.py` | Run each script from the `llama-index/` directory, and run `01_arxiv_pg` and `03_news_vector_rag` before `04_router`. Standalone notebooks live under [`examples/vector_stores/`](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/vector_stores) (vector, hybrid, metadata filtering) and [`examples/property_graph/`](https://github.com/skaiworldwide-oss/agensgraph-ai/tree/main/llama-index/examples/property_graph). Install `examples/demos/requirements-demos.txt`, copy `examples/demos/.env.example` to `examples/demos/.env`, and set connection settings and `OPENAI_API_KEY` there; the datasets stream from Hugging Face. Start with small `*_LIMIT` values and scale up once a demo runs end to end.