Package: neo4j-haystack
neo4j-haystackAHaystackDocument Store forNeo4j.Table of ContentsOverviewInstallationUsageLicenseOverviewAn integration ofNeo4jgraph database withHaystack v2.0bydeepset. In Neo4jVector search indexis being used for storing document embeddings and dense retrievals.The library allows using Neo4j as aDocumentStore, and implements the requiredProtocolmethods. You can start working with the implementation by importing it fromneo4_haystackpackage:fromneo4j_haystackimportNeo4jDocumentStoreIn addition to theNeo4jDocumentStorethe library includes the following haystack components which can be used in a pipeline:Neo4jEmbeddingRetriever- is a typicalretriever componentwhich can be used to query vector store index and find related Documents. The component usesNeo4jDocumentStoreto query embeddings.Neo4jDynamicDocumentRetrieveris also a retriever component in a sense that it can be used to query Documents in Neo4j. However it is decoupled fromNeo4jDocumentStoreand allows to run arbitraryCypher queryto extract documents. Practically it is possible to query Neo4j same wayNeo4jDocumentStoredoes, including vector search.Theneo4j-haystacklibrary usesPython DriverandCypher Queriesto interact with Neo4j database and hide all complexities under the hood.Neo4jDocumentStorewill store Documents as Graph nodes in Neo4j. Embeddings are stored as part of the node, but indexing and querying of vector embeddings using ANN is managed by a dedicatedVector Index.+-----------------------------+
                                   |       Neo4j Database        |
                                   +-----------------------------+
                                   |                             |
                                   |      +----------------+     |
                                   |      |    Document    |     |
                write_documents    |      +----------------+     |
          +------------------------+----->|   properties   |     |
          |                        |      |                |     |
+---------+----------+             |      |   embedding    |     |
|                    |             |      +--------+-------+     |
| Neo4jDocumentStore |             |               |             |
|                    |             |               |index/query  |
+---------+----------+             |               |             |
          |                        |      +--------+--------+    |
          |                        |      |  Vector Index   |    |
          +----------------------->|      |                 |    |
               query_embeddings    |      | (for embedding) |    |
                                   |      +-----------------+    |
                                   |                             |
                                   +-----------------------------+In the above diagram:Documentis a Neo4j node (with "Document" label)propertiesare Documentattributesstored as part of the node.In current implementationmetaattributes are stored on the same level as the rest of Document fields.embeddingis also a property of the Document node (just shown separately in the diagram for clarity) which is a vector of typeLIST[FLOAT].Vector Indexis where embeddings are getting indexed by Neo4j as soon as those are updated in Document nodes.Neo4jDocumentStoreby default creates a vector index if it does not exist. Before writing documents you should make sure Documents are embedded by one of the providedembedders. For exampleSentenceTransformersDocumentEmbeddercan be used in indexing pipeline to calculate document embeddings before writing those to Neo4j.Installationneo4j-haystackcan be installed as any other Python library, using pip:pipinstall--upgradepip# optionalpipinstallsentence-transformers# required in order to run pipeline examples given belowpipinstallneo4j-haystackWarningTheneo4j-haystackpackage currently uses Haystack 2.0-Beta, an unstable version of what will eventually become Haystack 2.0.
It will be updated and tested with the latest changes periodically until a stable version of the Haystack is released.UsageRunning Neo4jYou will need to have a running instance of Neo4j database to use components from the package (in-memory version of Neo4j is not supported).
There are several options available:Docker, other options available in the same Operations ManualAuraDB- a fully managed Cloud Instance of Neo4jNeo4j Desktopclient applicationThe simplest way to start database locally will be with Docker container:dockerrun\--restartalways\--publish=7474:7474--publish=7687:7687\--envNEO4J_AUTH=neo4j/passw0rd\neo4j:5.15.0As of Neo4j5.13, the vector search index is no longer a beta feature, consider using a version of the database">= 5.13". In the example above version5.15.0is being used to start a container. You could explore Known issues and Limitations in thedocumentation.TheNEO4J_AUTHenvironment variable sets default credentials (username/password) for authentication.NoteAssuming you have a docker container running navigate tohttp://localhost:7474to openNeo4j Browserto explore graph data and run Cypher queries.Document StoreOnce you have the package installed and the database running, you can start usingNeo4jDocumentStoreas any other document stores that support embeddings.fromneo4j_haystackimportNeo4jDocumentStoredocument_store=Neo4jDocumentStore(url="bolt://localhost:7687",username="neo4j",password="passw0rd",database="neo4j",embedding_dim=384,embedding_field="embedding",index="document-embeddings",# The name of the Vector Index in Neo4jnode_label="Document",# Providing a label to Neo4j nodes which store Documents)Alternatively, Neo4j connection properties could be specified using a dedicatedNeo4jClientConfigclass:fromneo4j_haystackimportNeo4jClientConfig,Neo4jDocumentStoreclient_config=Neo4jClientConfig(url="bolt://localhost:7687",username="neo4j",password="passw0rd",database="neo4j",)document_store=Neo4jDocumentStore(client_config=client_config,embedding_dim=384)Assuming there is a list of documents available and a running Neo4j database you can write/index those in Neo4j, e.g.:fromhaystackimportDocumentdocuments=[Document(content="My name is Morgan and I live in Paris.")]document_store.write_documents(documents)If you intend to obtain embeddings before writing documents use the following code:fromhaystackimportDocument# import one of the available document embeddersfromhaystack.components.embeddersimportSentenceTransformersDocumentEmbedderdocuments=[Document(content="My name is Morgan and I live in Paris.")]document_embedder=SentenceTransformersDocumentEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")document_embedder.warm_up()# will download the model during first rundocuments_with_embeddings=document_embedder.run(documents)document_store.write_documents(documents_with_embeddings.get("documents"))Make sure embedding model produces vectors of same size as it has been set onNeo4jDocumentStore, e.g. settingembedding_dim=384would comply with the "sentence-transformers/all-MiniLM-L6-v2" model.NoteMost of the time you will be usingHaystack Pipelinesto build both indexing and querying RAG scenarios.It is important to understand how haystack Documents are stored in Neo4j after you callwrite_documents.fromrandomimportrandomsample_embedding=[random()for_inrange(384)]# using fake/random embedding for brevity here to simplify exampledocument=Document(content="My name is Morgan and I live in Paris.",embedding=sample_embedding,meta={"num_of_years":3})document.to_dict()The above code converts a Document to a dictionary and will render the following output:>>>output:{"id":"11c255ad10bff4286781f596a5afd9ab093ed056d41bca4120c849058e52f24d","content":"My name is Morgan and I live in Paris.","dataframe":None,"blob":None,"score":None,"embedding":[0.025010755222666936,0.27502931836911926,0.22321073814882275,...],# vector of size 384"num_of_years":3,}The data from the dictionary will be used to create a node in Neo4j after you write the document withdocument_store.write_documents([document]). You could query it with Cypher, e.g.MATCH (doc:Document) RETURN doc. Below is a json representation of the node in Neo4j:{"identity":0,"labels":["Document"// label name is specified in the Neo4jDocumentStore.node_label argument],"properties":{// this is where Document data is stored"id":"11c255ad10bff4286781f596a5afd9ab093ed056d41bca4120c849058e52f24d","embedding":[0.6394268274307251,0.02501075528562069,0.27502933144569397,...],// vector of size 384"content":"My name is Morgan and I live in Paris.","num_of_years":3},"elementId":"4:8bde9fb3-3975-4c3e-9ea1-3e10dbad55eb:0"}NoteMetadata (num_of_years) is serialized to the same level as rest of attributes (flatten).It is expected by current implementationas Neo4j node's properties can not have nested structures.The full list of parameters accepted byNeo4jDocumentStorecan be found inAPI documentation.Indexing documentsWith Haystack you can useDocumentWritercomponent to write Documents into a Document Store. In the example below we construct pipeline to write documents to Neo4j usingNeo4jDocumentStore:fromhaystackimportDocumentfromhaystack.components.embeddersimportSentenceTransformersDocumentEmbedderfromhaystack.components.writersimportDocumentWriterfromhaystack.pipelineimportPipelinefromneo4j_haystackimportNeo4jDocumentStoredocuments=[Document(content="This is document 1"),Document(content="This is document 2")]document_store=Neo4jDocumentStore(url="bolt://localhost:7687",username="neo4j",password="passw0rd",database="neo4j",embedding_dim=384,embedding_field="embedding",index="document-embeddings",node_label="Document",)embedder=SentenceTransformersDocumentEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")document_writer=DocumentWriter(document_store=document_store)indexing_pipeline=Pipeline()indexing_pipeline.add_component(instance=embedder,name="embedder")indexing_pipeline.add_component(instance=document_writer,name="writer")indexing_pipeline.connect("embedder","writer")indexing_pipeline.run({"embedder":{"documents":documents}})>>>output:`{'writer':{'documents_written':2}}`Retrieving documentsNeo4jEmbeddingRetrievercomponent can be used to retrieve documents from Neo4j by querying vector index using an embedded query. Below is a pipeline which finds documents using query embedding as well asmetadata filtering:fromtypingimportListfromhaystackimportDocument,Pipelinefromhaystack.components.embeddersimportSentenceTransformersDocumentEmbedder,SentenceTransformersTextEmbedderfromneo4j_haystackimportNeo4jDocumentStore,Neo4jEmbeddingRetrieverdocument_store=Neo4jDocumentStore(url="bolt://localhost:7687",username="neo4j",password="passw0rd",database="neo4j",embedding_dim=384,index="document-embeddings",)documents=[Document(content="My name is Morgan and I live in Paris.",meta={"num_of_years":3}),Document(content="I am Susan and I live in Berlin.",meta={"num_of_years":7}),]# Same model is used for both query and Document embeddingsmodel_name="sentence-transformers/all-MiniLM-L6-v2"document_embedder=SentenceTransformersDocumentEmbedder(model=model_name)document_embedder.warm_up()documents_with_embeddings=document_embedder.run(documents)document_store.write_documents(documents_with_embeddings.get("documents"))print("Number of documents written: ",document_store.count_documents())pipeline=Pipeline()pipeline.add_component("text_embedder",SentenceTransformersTextEmbedder(model=model_name))pipeline.add_component("retriever",Neo4jEmbeddingRetriever(document_store=document_store))pipeline.connect("text_embedder.embedding","retriever.query_embedding")result=pipeline.run(data={"text_embedder":{"text":"What cities do people live in?"},"retriever":{"top_k":5,"filters":{"field":"num_of_years","operator":"==","value":3},},})documents:List[Document]=result["retriever"]["documents"]>>>output:[Document(id=3930326edabe6d172031557556999e2f8ba258ccde3c876f5e3ac7e66ed3d53a,content:'My name is Morgan and I live in Paris.',meta:{'num_of_years':3},score:0.8348373770713806)]NoteYou can learn more about how a given metadata filter is converted into Cypher queries by looking at documentation of theNeo4jQueryConverterclass.Retrieving documents using CypherIn certain scenarios you might have an existing graph in Neo4j database which was created by custom scripts or data ingestion pipelines. The schema of the graph could be complex and not exactly fitting into Haystack Document model. Moreover in many situations you might want to leverage existing graph data to extract more context for grounding LLMs. To make it possible with Haystack we haveNeo4jDynamicDocumentRetrievercomponent - a flexible retriever which can run arbitrary Cypher query to obtain documents. This component does not require Document Store to operate.The above example ofNeo4jEmbeddingRetrievercould be rewritten without usage ofNeo4jDocumentStorein the retrieval pipeline:fromtypingimportListfromhaystackimportDocument,Pipelinefromhaystack.components.embeddersimportSentenceTransformersDocumentEmbedder,SentenceTransformersTextEmbedderfromneo4j_haystackimportNeo4jClientConfig,Neo4jDocumentStore,Neo4jDynamicDocumentRetrieverclient_config=Neo4jClientConfig(url="bolt://localhost:7687",username="neo4j",password="passw0rd",database="neo4j",)documents=[Document(content="My name is Morgan and I live in Paris.",meta={"num_of_years":3}),Document(content="I am Susan and I live in Berlin.",meta={"num_of_years":7}),]# Same model is used for both query and Document embeddingsmodel_name="sentence-transformers/all-MiniLM-L6-v2"document_embedder=SentenceTransformersDocumentEmbedder(model=model_name)document_embedder.warm_up()documents_with_embeddings=document_embedder.run(documents)document_store=Neo4jDocumentStore(client_config=client_config,embedding_dim=384)document_store.write_documents(documents_with_embeddings.get("documents"))# Same model is used for both query and Document embeddingsmodel_name="sentence-transformers/all-MiniLM-L6-v2"cypher_query="""CALL db.index.vector.queryNodes($index, $top_k, $query_embedding)YIELD node as doc, scoreMATCH (doc) WHERE doc.num_of_years = $num_of_yearsRETURN doc{.*, score}, scoreORDER BY score DESC LIMIT $top_k"""embedder=SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")retriever=Neo4jDynamicDocumentRetriever(client_config=client_config,runtime_parameters=["query_embedding"],doc_node_name="doc")pipeline=Pipeline()pipeline.add_component("text_embedder",embedder)pipeline.add_component("retriever",retriever)pipeline.connect("text_embedder.embedding","retriever.query_embedding")result=pipeline.run(data={"text_embedder":{"text":"What cities do people live in?"},"retriever":{"query":cypher_query,"parameters":{"index":"document-embeddings","top_k":5,"num_of_years":3},},})documents:List[Document]=result["retriever"]["documents"]>>>output:[Document(id=4014455c3be5d88151ba12d734a16754d7af75c691dfc3a5f364f81772471bd2,content:'My name is Morgan and I live in Paris.',meta:{'num_of_years':3},score:0.6696747541427612,embedding:vectorofsize384)]Please notice how query parameters are being used in thecypher_query:runtime_parametersis a list of parameter names which are going to be input slots when connecting components
in a pipeline. In our casequery_embeddinginput is connected to thetext_embedder.embeddingoutput.pipeline.runspecifies additional parameters to theretrievercomponent which can be referenced in thecypher_query, e.g.top_kandnum_of_years.In some wayNeo4jDynamicDocumentRetrieverresembles theDynamicPromptBuildercomponent, only instead of prompt it constructs a Cypher query usingparameters. In the example above documents retrieved by running the query, theRETURN doc{.*, score}part returns back found documents with scores. Which node variable is going to be used to construct haystack Document is specified in thedoc_node_nameparameter (see abovedoc_node_name="doc").You have options to enhance your RAG pipeline with data having various schemas, for example by first finding nodes using vector search and then expanding query to search for nearby nodes using appropriate Cypher syntax. It is possible to implement "Parent-Child" chunking strategy with such approach. Before that you have to ingest/index data into Neo4j accordingly by building an indexing pipeline or a custom ingestion script. A simple schema is shown below:┌────────────┐                ┌─────────────┐
│   Child    │                │   Parent    │
│            │  :HAS_PARENT   │             │
│   content  ├────────────────┤   content   │
│  embedding │                │             │
└────────────┘                └─────────────┘The following Cypher query is an example of howNeo4jDynamicDocumentRetrievercan first search embeddings forChilddocument chunks and thenreturnParentdocuments which have larger context window (text length) for RAG applications:// Query Child documents by $query_embeddingCALLdb.index.vector.queryNodes($index,$top_k,$query_embedding)YIELDnodeaschild_doc,score// Find Parent document for previously retrieved child (e.g. extend RAG context)MATCH(child_doc)-[:HAS_PARENT]->(parent:Parent)WITHparent,max(score)ASscore// deduplicate parentsRETURNparent{.*,score}As you might have guessed, the value for thedoc_node_nameparameter should be equal toparentaccording to the query above.More examplesYou can find more examples in the implementationrepository:indexing_pipeline.py- Indexing text files (documents) from a remote http location.rag_pipeline.py- Generative question answering RAG pipeline usingNeo4jEmbeddingRetrieverto fetch documents from Neo4j document store and answer question usingHuggingFaceTGIGenerator.rag_pipeline_cypher.py- Same asrag_pipeline.pybut usingNeo4jDynamicDocumentRetriever.More technical details available in theCode Referencedocumentation. For example, in real world scenarios there could be requirements to tune connection settings to Neo4j database (e.g. request timeout).Neo4jDocumentStoreaccepts an extended client configuration usingNeo4jClientConfigclass.Licenseneo4j-haystackis distributed under the terms of theMITlicense.