From c9adf73d695e8cec6e842a36945e2acebc8a8878 Mon Sep 17 00:00:00 2001 From: Julian Risch Date: Mon, 11 May 2026 12:54:03 +0200 Subject: [PATCH] feat: add FalkorDB integration page Adds integration page and logo for the FalkorDB document store integration (falkordb-haystack), including FalkorDBDocumentStore, FalkorDBEmbeddingRetriever, and FalkorDBCypherRetriever components. Co-Authored-By: Claude Sonnet 4.6 --- integrations/falkordb.md | 194 +++++++++++++++++++++++++++++++++++++++ logos/falkordb.png | Bin 0 -> 43944 bytes 2 files changed, 194 insertions(+) create mode 100644 integrations/falkordb.md create mode 100644 logos/falkordb.png diff --git a/integrations/falkordb.md b/integrations/falkordb.md new file mode 100644 index 00000000..def53e1e --- /dev/null +++ b/integrations/falkordb.md @@ -0,0 +1,194 @@ +--- +layout: integration +name: FalkorDB +description: Use FalkorDB as a document store with native vector search for GraphRAG workloads in Haystack +authors: + - name: deepset + socials: + github: deepset-ai + twitter: deepset_ai + linkedin: https://www.linkedin.com/company/deepset-ai/ +pypi: https://pypi.org/project/falkordb-haystack/ +repo: https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/falkordb +type: Document Store +report_issue: https://github.com/deepset-ai/haystack-core-integrations/issues +logo: /logos/falkordb.png +version: Haystack 2.0 +toc: true +--- + +### Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Usage](#usage) + - [Writing documents](#writing-documents) + - [Retrieving documents](#retrieving-documents) + - [Graph queries with Cypher](#graph-queries-with-cypher) +- [License](#license) + +## Overview + +An integration of [FalkorDB](https://www.falkordb.com/) with [Haystack](https://docs.haystack.deepset.ai/docs/intro) by [deepset](https://www.deepset.ai). + +FalkorDB is a high-performance graph database optimized for GraphRAG workloads. It stores documents as graph nodes and supports native vector search — no APOC is required. All bulk writes use `UNWIND` + `MERGE` for safe, idiomatic OpenCypher upserts. + +The library provides a `FalkorDBDocumentStore` that implements the Haystack [DocumentStore protocol](https://docs.haystack.deepset.ai/docs/document-store#documentstore-protocol), plus two pipeline-ready retriever components: + +- **FalkorDBDocumentStore** — stores Documents as labeled graph nodes in a named FalkorDB graph, with `meta` fields stored flat alongside `id` and `content`. Embeddings are indexed using FalkorDB's native vector index. +- **FalkorDBEmbeddingRetriever** — a [retriever component](https://docs.haystack.deepset.ai/docs/retrievers) that queries the native vector index to find Documents by dense similarity, with support for metadata filtering. +- **FalkorDBCypherRetriever** — a power-user retriever for executing arbitrary [OpenCypher](https://opencypher.org/) queries, enabling graph traversal and multi-hop queries in GraphRAG pipelines. + +```text + +-----------------------------+ + | FalkorDB Database | + +-----------------------------+ + | | + | +----------------+ | + | | Document | | + write_documents | +----------------+ | + +------------------------+----->| properties | | + | | | | | ++---------+----------+ | | embedding | | +| | | +--------+-------+ | +| FalkorDBDocument | | | | +| Store | | |index/query | ++---------+----------+ | | | + | | +---------+---------+ | + | | | Native Vector Idx | | + +----------------------->| | | | + _embedding_retrieval | | (vecf32 index) | | + | +-------------------+ | + | | + +-----------------------------+ +``` + +In the above diagram: + +- `Document` is a FalkorDB node with a configurable label (default: `"Document"`) +- `properties` are Document [attributes](https://docs.haystack.deepset.ai/docs/data-classes#document) and `meta` fields stored flat on the node +- `embedding` is stored as a `vecf32` vector property indexed by FalkorDB's native vector index +- The native vector index enables approximate nearest neighbor search via `db.idx.vector.queryNodes` + +## Installation + +`falkordb-haystack` can be installed using pip: + +```bash +pip install falkordb-haystack +``` + +You will need a running FalkorDB instance. The simplest way is with Docker: + +```bash +docker run -d -p 6379:6379 falkordb/falkordb:latest +``` + +## Usage + +```python +from haystack_integrations.document_stores.falkordb import FalkorDBDocumentStore + +document_store = FalkorDBDocumentStore( + host="localhost", + port=6379, + embedding_dim=384, + similarity="cosine", +) +``` + +### Writing documents + +```python +from haystack import Document +from haystack.document_stores.types import DuplicatePolicy + +documents = [ + Document( + content="FalkorDB is a high-performance graph database for GraphRAG.", + meta={"source": "docs", "category": "database"}, + ) +] +document_store.write_documents(documents, policy=DuplicatePolicy.OVERWRITE) +``` + +### Retrieving documents + +`FalkorDBEmbeddingRetriever` can be used in a pipeline to retrieve documents by querying the native vector index with an embedded query, with optional metadata filtering: + +```python +from haystack import Document, Pipeline +from haystack.components.embedders import ( + SentenceTransformersDocumentEmbedder, + SentenceTransformersTextEmbedder, +) +from haystack_integrations.document_stores.falkordb import FalkorDBDocumentStore +from haystack_integrations.components.retrievers.falkordb import FalkorDBEmbeddingRetriever + +document_store = FalkorDBDocumentStore( + host="localhost", + port=6379, + embedding_dim=384, + recreate_graph=True, +) + +documents = [ + Document( + content="My name is Morgan and I live in Paris.", + meta={"release_date": "2018-12-09"}, + ) +] + +document_embedder = SentenceTransformersDocumentEmbedder( + model="sentence-transformers/all-MiniLM-L6-v2" +) +document_embedder.warm_up() +documents_with_embeddings = document_embedder.run(documents) +document_store.write_documents(documents_with_embeddings["documents"]) + +pipeline = Pipeline() +pipeline.add_component( + "text_embedder", + SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2"), +) +pipeline.add_component( + "retriever", + FalkorDBEmbeddingRetriever(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": "release_date", "operator": "==", "value": "2018-12-09"}, + }, + } +) + +documents = result["retriever"]["documents"] +``` + +### Graph queries with Cypher + +`FalkorDBCypherRetriever` allows you to run arbitrary OpenCypher queries against the graph, which is useful for multi-hop traversals and custom GraphRAG patterns. Use parameterized queries to avoid injection vulnerabilities: + +```python +from haystack_integrations.document_stores.falkordb import FalkorDBDocumentStore +from haystack_integrations.components.retrievers.falkordb import FalkorDBCypherRetriever + +document_store = FalkorDBDocumentStore(host="localhost", port=6379) + +retriever = FalkorDBCypherRetriever( + document_store=document_store, + custom_cypher_query="MATCH (d:Document {topic: $topic}) RETURN d", +) + +result = retriever.run(parameters={"topic": "GraphRAG"}) +documents = result["documents"] +``` + +## License + +`falkordb-haystack` is distributed under the terms of the [Apache 2.0](https://spdx.org/licenses/Apache-2.0.html) license. diff --git a/logos/falkordb.png b/logos/falkordb.png new file mode 100644 index 0000000000000000000000000000000000000000..610f10b4b7af7b35428551ebc487f8654fff0c1b GIT binary patch literal 43944 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 Wf&hX5f&hX5f&hX5g24Yf0{