Back to blog
Migrate from FalkorDB: Move a GraphRAG Graph to HelixDB

This blog is written by AI for SEO

Migrate from FalkorDB: Move a GraphRAG Graph to HelixDB

HelixDB9 min read

Plenty of teams start their GraphRAG pipelines on FalkorDB. RedisGraph patterns felt familiar, and the FalkorDB GraphRAG SDK gave them a fast path to a proof-of-concept. FalkorDB runs as a Redis module (or a standalone Redis-compatible server) and executes Cypher queries against sparse adjacency matrices with GraphBLAS.

That design works well for pure in-memory graph traversals. But AI agent memory and enterprise knowledge graphs hit practical limits fast. A large vector index inside Redis needs massive, expensive RAM allocations. And when you want hybrid retrieval that mixes BM25 keyword matching, vector similarity, and multi-hop graph traversal, you end up coordinating several indices or bolting external services onto your Redis instance.

This guide walks through migrating from FalkorDB to HelixDB, an open-source graph-vector database built in Rust. You will extract your existing schema, batch export nodes and edges through FalkorDB query interfaces, map indexes to HelixDB native types, and translate Cypher retrieval into HelixDB JSON queries using the TypeScript SDK.

Step 1: Inventory your FalkorDB graph (labels, relationship types, properties)

Before touching any destination system, inspect the shape of your live FalkorDB instance. In a typical GraphRAG setup, your graph contains document nodes, chunk nodes, extracted entities, and topical communities.

Connect to FalkorDB using redis-cli or your application client and run structural introspection queries. FalkorDB has built-in procedures that list labels and relationships across a target graph key:

GRAPH.QUERY rag_db "CALL db.labels()"
GRAPH.QUERY rag_db "CALL db.relationshipTypes()"
GRAPH.QUERY rag_db "CALL db.propertyKeys()"

A standard GraphRAG deployment has a specific topology:

  • Node labels: Document, Chunk, Entity, Community

  • Relationship types: PART_OF (Chunk to Document), MENTIONS (Chunk to Entity), RELATES_TO (Entity to Entity), IN_COMMUNITY (Entity to Community)

  • Properties: id (string), text (string), name (string), description (string), embedding (vector array), created_at (integer timestamp), weight (float)

Note which properties carry high cardinality and which hold embeddings. In FalkorDB, vectors usually sit as lists of floats on Chunk or Entity nodes. Record the exact vector dimensions (commonly 1536 for OpenAI models, or 384 to 1024 for local embedding models) and the distance metrics configured on each label. Getting these types right now prevents parsing failures during ingestion.

Step 2: Capture your index DDL (range, vector, full-text) as FalkorDB documents it

FalkorDB supports range, vector, full-text, and CCH indexes: range indexes cover string, numeric, and geospatial properties, vector indexes support nearest-neighbor similarity search, and full-text indexes leverage RediSearch capabilities. Capture these explicitly, because an exported graph does not recreate indexes at the destination.

Query your existing indices with FalkorDB procedure calls:

GRAPH.QUERY rag_db "CALL db.indexes()"

The output lists every active index, its target label, indexed properties, and options. For GraphRAG workloads, your FalkorDB DDL looks like this:

-- Scalar range index for timestamp filtering
CREATE INDEX FOR (c:Chunk) ON (c.created_at);

-- Vector index on Chunk embeddings
CREATE VECTOR INDEX FOR (c:Chunk) ON (c.embedding) 
OPTIONS {dimension: 1536, similarityFunction: 'cosine'};

-- Full-text index on Entity text fields
CREATE FULLTEXT INDEX FOR (e:Entity) ON (e.name, e.description);

FalkorDB splits these indexes into separate execution modules inside the Redis process. The architectural difference matters when you compare HelixDB vs FalkorDB: FalkorDB keeps vector structures in memory next to its GraphBLAS matrices, while HelixDB runs graph traversal, native float32 vector indexes, and BM25 search inside a single storage engine backed by SlateDB. Write down your dimensions, distance functions (cosine, euclidean, or manhattan), and text search fields so you can declare them cleanly in Step 4.

Step 3: Export nodes and edges with batched GRAPH.QUERY

You can export a FalkorDB graph by streaming records through parameterized Cypher queries with GRAPH.QUERY. Large graphs will exhaust client buffers if you pull them in one transaction, so paginate the export with SKIP and LIMIT batches of 5,000 to 10,000 entities.

First, extract the node populations by label into newline-delimited JSON (NDJSON). Run a paginated extraction script across your nodes:

GRAPH.QUERY rag_db "MATCH (n:Entity) 
RETURN id(n) AS internal_id, n.id AS external_id, labels(n) AS labels, properties(n) AS props 
SKIP 0 LIMIT 10000"

Repeat this query, incrementing SKIP until zero rows return. Make sure your export script serializes the vector arrays directly and keeps full float precision. A representative exported node record in NDJSON looks like this:

{"internal_id": 1024, "external_id": "ent_987", "label": "Entity", "properties": {"name": "Vector Index", "description": "Data structure for similarity search", "created_at": 1718000000, "embedding": [0.012, -0.045, 0.089]}}

Second, export the edges. Edge extraction must reference stable identifiers. In FalkorDB, internal node IDs (id(n)) can change across reloads or cluster rebalancing, so bind your edges to your domain keys (external_id or n.id):

GRAPH.QUERY rag_db "MATCH (src)-[r]->(tgt) 
RETURN src.id AS source_id, tgt.id AS target_id, type(r) AS rel_type, properties(r) AS props 
SKIP 0 LIMIT 10000"

Write the extracted edges to a separate edges.ndjson file. Separate node and edge streams let you validate each one cleanly before loading.

Step 4: Map the schema to HelixDB (node/edge types, float32 vector property, BM25 text index)

HelixDB treats knowledge graphs, vector search, and full-text search as properties of a single schema. You don't stitch together separate search modules or configure separate storage backends.

Translate your FalkorDB definitions to HelixDB concepts with three rules:

  1. Top-level vector property: In HelixDB, vector properties must be declared as top-level fields on nodes or edges. Values normalize to float32 automatically. Pick your metric: cosine, euclidean, or manhattan.

  2. Native BM25 full-text indexing: Instead of external search module syntax, define BM25 full-text indexes directly on target string properties.

  3. Native time-range indexing: HelixDB indexes numeric timestamps efficiently, so agent memory systems can prune or window graphs without full table scans.

Map the FalkorDB schema items from Step 2 to HelixDB:

  • FalkorDB (c:Chunk) vector index -> HelixDB top-level float32 vector property on Chunk nodes using cosine distance.

  • FalkorDB (e:Entity) fulltext index -> HelixDB BM25 text index on name and description.

  • FalkorDB (c:Chunk.created_at) range index -> HelixDB time-range index on created_at.

Some systems make you pre-compile specialized query endpoints. HelixDB doesn't. It accepts dynamic JSON queries submitted from application code, parses them at runtime, and translates them into engine execution steps.

Step 5: Stand up HelixDB locally with the Helix CLI, then load the export

HelixDB ships a lightweight CLI for local development and self-hosting. Install the CLI and initialize a new project workspace:

helix init graphrag-migration
cd graphrag-migration
helix start dev

This boots the HelixDB local development server at http://localhost:8080. HelixDB can run fully in memory, on disk, or backed by S3-compatible object storage via SlateDB.

Next, write a migration loader with the official TypeScript SDK (@helix-db/helix-db). The loader reads the exported NDJSON files and issues batched transactional writes:

import { writeBatch, PropertyInput } from "@helix-db/helix-db";
import * as fs from "fs";
import * as readline from "readline";

async function loadNodes() {
  const fileStream = fs.createReadStream("nodes.ndjson");
  const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
  
  let batch = [];
  for await (const line of rl) {
    const node = JSON.parse(line);
    batch.push({
      label: node.label,
      external_id: node.external_id,
      properties: {
        ...node.properties,
        // Float vectors normalize automatically to float32
        embedding: node.properties.embedding
      }
    });
    
    if (batch.length >= 1000) {
      await writeBatch({
        endpoint: "http://localhost:8080/v2/query",
        nodes: batch
      });
      batch = [];
    }
  }
  if (batch.length > 0) {
    await writeBatch({ endpoint: "http://localhost:8080/v2/query", nodes: batch });
  }
}

loadNodes();

Repeat this process for edges.ndjson, resolving source and target references against your node IDs. HelixDB commits graph structures and vector indices atomically, so nodes and their vector properties never drift out of sync.

Step 6: Rewrite Cypher retrieval as JSON queries via the TypeScript SDK to POST /v2/query

In FalkorDB, a standard GraphRAG query finds seed chunks by vector similarity, traverses related entities, and aggregates connected document summaries in Cypher:

// FalkorDB Cypher
CALL db.idx.vector.queryNodes('Chunk', 'embedding', 5, vec_param) YIELD node AS chunk
MATCH (chunk)-[:MENTIONS]->(e:Entity)-[:RELATES_TO]->(target:Entity)
RETURN chunk.text, e.name, target.name

In HelixDB, you don't write Cypher strings. Your application builds JSON queries with the TypeScript SDK, or submits raw JSON payloads to POST /v2/query. HelixDB supports vector pre-filtering, so you can scope similarity searches to related graph components instead of scanning the entire global index.

Here is the translated retrieval logic in TypeScript:

import { g, VectorDistanceMetric } from "@helix-db/helix-db";

async function queryGraphRAG(queryEmbedding: number[]) {
  const query = g
    .nodes("Chunk")
    .vectorSearch({
      property: "embedding",
      vector: queryEmbedding,
      metric: VectorDistanceMetric.Cosine,
      limit: 5
    })
    .out("MENTIONS")
    .hasLabel("Entity")
    .out("RELATES_TO")
    .hasLabel("Entity")
    .project(["name", "description"]);

  const response = await fetch("http://localhost:8080/v2/query", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(query.toJSON())
  });

  return await response.json();
}

The same pattern extends to combined keyword and embedding retrieval. Our guide on how to combine BM25 and vector search in one query for RAG covers it. HelixDB parses the JSON payload straight into its execution plan, traversing graph edges and filtering vector candidates in a single pass.

Step 7: Verify parity, then cut over (and when to stay on FalkorDB)

Before you decommission your FalkorDB instance, verify structural and retrieval parity across both systems.

Run counts on both stores to confirm identical entity counts:

  • Total nodes per label (Document, Chunk, Entity)

  • Total edges per relationship type (MENTIONS, RELATES_TO)

  • Spot checks on 100 sample vectors to confirm dimensions and vector distance scores match

Keep the documented engine limits in mind. In HelixDB, an unrestricted vector search caps at 800 effective results. For traversal-scoped searches, HelixDB applies that ceiling after intersecting candidate sets and rejects queries that exceed limits instead of silently clamping output. A query candidate stream above 1,000,000 unique entities also triggers a query error to protect OLTP stability.

Three concrete operational differences justify switching to HelixDB:

  1. Memory economics: FalkorDB keeps its dataset and indexes in RAM. HelixDB is built on SlateDB, so storage scales across memory, local NVMe disk, and S3-compatible object storage while hot working sets stay cached.

  2. Unified multi-modal engine: BM25 full-text search, graph traversal, and native vector search live in one engine. No external Redis modules to stitch together.

  3. Application-native querying: The TypeScript SDK emits clean JSON queries directly to POST /v2/query. That removes Cypher string templating and its escape bugs.

When should you stay on FalkorDB? Keep it if your architecture is already deeply coupled to Redis infrastructure, your entire dataset fits comfortably in RAM, and your queries rely only on GraphBLAS sparse matrix linear algebra, where pure in-memory matrix multiplication beats disk-aware graph engines.

Conclusion

Migrating your GraphRAG graph off FalkorDB gives you one database built for AI memory. You stop paying the operational tax of holding massive vector indexes in expensive RAM, and you gain native BM25 search, vector pre-filtering, and clean JSON queries.

To build your next retrieval pipeline without duct tape, read our guide on how to build a GraphRAG pipeline. Star the HelixDB repository on GitHub, install the Helix CLI, and test your migrated graph locally in minutes.

Build with HelixDB

Give your coding agent the setup prompt, or sign up and deploy a database.

Sign up