Unstructured2Graph
Unstructured2Graph turns documents, such as reports, PDFs, research papers or meeting notes, into a knowledge graph in Memgraph that an LLM can query. It’s part of the Memgraph AI Toolkit.
It combines two parts:
- Unstructured IO extracts, cleans and chunks documents in formats such as PDF, DOCX and TXT.
- An extraction backend finds entities and relationships in the chunks and writes them to Memgraph.
You choose the extraction backend:
- GLiNER2 (default): A local model, with no LLM calls and no API key. It extracts the types listed in its ontology, a file of entity and relationship types. The default ontology lists entity types such as people, organizations and locations, and no relationship types, so GLiNER2 extracts relationships only with an ontology of your own.
- LightRAG: An LLM extracts entities and any relationships it finds, and merges mentions of the same entity across chunks. Each chunk costs at least one LLM call.
The result is a graph of entities, relationships and embeddings, ready for GraphRAG.
Getting started
Follow these steps to create your first knowledge graph from documents.
Start Memgraph
Start by preparing your workspace and running Memgraph locally using Docker:
docker run -p 7687:7687 -p 7444:7444 --name memgraph memgraph/memgraph-mageOpen your terminal, VS Code, Cursor, or any other development environment you prefer. This is where you’ll run Python scripts connected to your Memgraph instance.
You are now ready to start building your graph.
Install Unstructured2Graph
Install Unstructured2Graph from PyPI. It needs Python 3.10 or later, and it installs PyTorch for the GLiNER2 model, which downloads on first use:
pip install unstructured2graphTo parse PDF, DOCX and other document formats, install it with the all-docs
extra instead. The example below reads a PDF, so it needs the extra:
pip install "unstructured2graph[all-docs]"Configure environment variables
GLiNER2 needs no API key. The LightRAG backend and the GraphRAG query below call OpenAI, so for those, set your OpenAI API key:
export OPENAI_API_KEY=YOUR_OPENAI_API_KEYReplace YOUR_OPENAI_API_KEY with your key from the OpenAI platform.
Ingest documents
Unstructured2Graph reads local files and URLs in formats such as PDF, DOCX, TXT and HTML. It extracts the readable text, removes elements such as headers and page numbers, and splits the text into chunks by document layout. The extraction backend then reads each chunk.
This example ingests a local file and a web page with GLiNER2 and creates embeddings for search:
import asyncio
import logging
from memgraph_toolbox.api.memgraph import Memgraph
from unstructured2graph import from_unstructured, compute_embeddings, create_vector_search_index
from unstructured2graph.gliner2_backend import GLiNER2Backend
async def ingest_documents():
# Connect to Memgraph and clear existing data
memgraph = Memgraph()
memgraph.query("MATCH (n) DETACH DELETE n;")
# Extract entities with a local GLiNER2 model, downloaded on first use
backend = GLiNER2Backend()
# Define your document sources
sources = [
"docs/paper.pdf", # local file
"https://example.com/page.html" # remote URL
]
# Chunk the documents, extract entities and write them to Memgraph
await from_unstructured(
sources=sources,
memgraph=memgraph,
extraction_backend=backend,
link_chunks=True, # link chunks in order with NEXT relationships
enforce_ontology=True, # add each entity's type as a label, such as :Person
)
# Create embeddings and a vector index for semantic search
compute_embeddings(memgraph, "Chunk")
create_vector_search_index(memgraph, "Chunk", "embedding")
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(ingest_documents())To extract with LightRAG instead, wrap a LightRAG instance in
LightRAGBackend and pass it as the backend. Replace the backend and
from_unstructured() lines in the example with:
from lightrag_memgraph import MemgraphLightRAGWrapper
from unstructured2graph import LightRAGBackend
lightrag = MemgraphLightRAGWrapper()
await lightrag.initialize(working_dir="./lightrag_storage")
await from_unstructured(
sources=sources,
memgraph=memgraph,
extraction_backend=LightRAGBackend(lightrag),
link_chunks=True,
)
await lightrag.afinalize()Here’s what happens step by step:
- Unstructured IO extracts, cleans and chunks the text.
- Each chunk becomes a
Chunknode with properties such ashashandtext. Unstructured2Graph creates a uniqueness constraint and an index onChunk.hash. - The backend extracts entities. GLiNER2 writes them as
gliner2nodes with anentity_typeproperty, and withenforce_ontology=Trueadds the type as a label, such asPerson. LightRAG writesbasenodes. - Entities are linked to the chunks that mention them with
MENTIONED_INrelationships. - Chunks are linked in order with
NEXTrelationships. compute_embeddings()stores an embedding on each chunk, andcreate_vector_search_index()creates thevs_namevector index on it.
After processing, your Memgraph instance will hold a complete, queryable knowledge graph ready for GraphRAG.
Query with GraphRAG
Once your data is ingested, you can perform GraphRAG retrieval directly inside Memgraph with a single query. This combines semantic search with graph traversal to retrieve the most relevant context for your questions.
import os
from memgraph_toolbox.api.memgraph import Memgraph
from openai import OpenAI
def graphrag_query(prompt: str):
memgraph = Memgraph()
# Retrieve relevant chunks using vector search + graph traversal
retrieved_chunks = []
for row in memgraph.query(
"""
CALL embeddings.text([$prompt]) YIELD embeddings, success
CALL vector_search.search('vs_name', 5, embeddings[0]) YIELD distance, node, similarity
OPTIONAL MATCH (node)-[r*bfs]-(dst:Chunk)
WITH collect(DISTINCT node) + collect(DISTINCT dst) AS chunks
UNWIND chunks AS chunk
WITH DISTINCT chunk, degree(chunk) AS degree ORDER BY degree DESC
RETURN chunk AS dst LIMIT 5;
""",
params={"prompt": prompt},
):
if "text" in row["dst"]:
retrieved_chunks.append(row["dst"]["text"])
if not retrieved_chunks:
print("No chunks retrieved.")
return
# Send retrieved context to LLM for summarization
context = "\n\n".join(retrieved_chunks)
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
completion = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Answer the question based on the provided context."},
{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {prompt}"},
],
temperature=0.1,
)
answer = completion.choices[0].message.content
print(f"Question: {prompt}")
print(f"Answer: {answer}")
if __name__ == "__main__":
graphrag_query("What are the key findings in the document?")Here’s what the GraphRAG query does:
- Converts the input prompt, passed as the
$promptparameter, into an embedding. - Searches for the most semantically relevant chunks using vector search.
- Adds the chunks connected to them in the graph, found with BFS traversal, and keeps the five with the most relationships.
- Sends the retrieved text to an LLM for summarization or question answering.
Visualize the graph in Memgraph Lab
Open Memgraph Lab and connect to your local instance. Then, in the Query Execution tab, run:
MATCH (n)-[r]->(m) RETURN n, r, m;You’ll see:
Chunknodes for text sectionsgliner2nodes for extracted entities, labeled with their type, such asPerson(basenodes with LightRAG)MENTIONED_INrelationships linking entities to their source chunksNEXTrelationships connecting sequential chunks
Explore this graph visually to understand how your content has been transformed into a connected network of knowledge. From here, you can keep querying and navigating the graph to uncover additional context, run advanced analyses, or expand your knowledge base as needed.
Try it in Memgraph Cloud
Want to skip local setup? You can also use Unstructured2Graph directly with Memgraph Cloud. Sign up, create a new project, and start building your knowledge graph in minutes.