Skip to main content

Overview

Experio stores the knowledge graph (ontology instances, document relationships, embeddings on graph nodes) against either Neo4j or FalkorDB. Exactly one backend is active per deployment. The application routes reads and writes through a shared abstraction layer so ingestion, chat retrieval, and admin tools stay consistent.

Choosing the active backend

Set GRAPH_PROVIDER to neo4j or falkordb in Admin > Settings > System Settings (DB category). Connection fields for the inactive provider remain stored but are ignored until you switch. See the DB settings reference for all keys. After changing provider or connection details, validate connectivity using the dashboard flow (including the reachability probe surfaced when switching providers).

Graph migration (staff)

Staff can copy data between backends from Admin > Settings > Graph migration:
  • Neo4j → FalkorDB and FalkorDB → Neo4j jobs stream progress over a WebSocket.
  • Use prerequisites on that page (backup, connectivity) before starting a long run.
Product and engineering decisions for the reverse direction are documented in FalkorDB → Neo4j migration research.

Behavioral differences

Node ids on FalkorDB

On FalkorDB a node’s id is the whole number that id(n) returns. Experio passes it around as text (for example the element_id of the lineage API) and sends it back to FalkorDB as a number when it looks a node up by id. If you read or write node ids, three things matter:
  • Compare id(n) with a number. On FalkorDB 6.0.3, WHERE id(n) = '12345' fails with Node ID must be an integer. FalkorDB 4.22.1 returned no rows for the same query and said nothing, so a query with a text id that looked harmless there fails now. Write id(n) = 12345, or compare text with toString(id(n)) = '12345'. Neo4j element ids are unchanged: they stay text.
  • 0 is a real node. A graph starts at node 0, and FalkorDB 6.x hands out the lowest free id first, so after a delete the next node can take 0 again. Only a missing or blank id means “no node”; never read 0 as missing.
  • An id names a node only while the node exists. FalkorDB reuses the ids of deleted nodes (6.x takes the lowest free id, 4.22.1 took the one freed most recently), so an id you noted earlier can later name a different node. Do not keep a node id as a long-term key outside Experio; use a property the node carries, such as its name.
When Experio itself looks nodes up (ingestion, enrichment, document search and the chat lookups), a reference that is not a whole number, such as an old Neo4j element id left in a saved review, finds nothing instead of raising an error. Document search also passes its document ids to FalkorDB as numbers now, so the entities that the documents it finds mention come back with the results; before, that lookup matched nothing on FalkorDB.

FalkorDB resilience & query limits

  • Per-query memory cap — FalkorDB is started with QUERY_MEM_CAPACITY set to 512 MiB by default (earlier releases used 1 GiB). A single query that exceeds the cap is terminated by FalkorDB with Query's mem consumption exceeded capacity instead of pushing the container toward its memory limit (and an OOM restart). If a legitimate analytical query hits the cap, narrow the query; the cap is the FalkorDB Query Memory Cap deployment parameter, which reaches FalkorDB through FALKORDB_ARGS (see FalkorDB deployment parameters).
  • Per-query runtime cap — TIMEOUT_DEFAULT is set to 60 seconds. FalkorDB’s default is 0 (unlimited). Queries that omit an explicit TIMEOUT (including an unbounded FalkorDB Browser MATCH (n) RETURN n) are killed at 60s instead of holding the graph lock indefinitely while Redis PING still succeeds. 60s matches the Experio client socket_timeout. Do not set the deprecated TIMEOUT alongside TIMEOUT_DEFAULT — the combination aborts module load. TIMEOUT_MAX is left at 0 so a client may pass a longer per-query TIMEOUT when needed.
  • Interactive read limit — a read that a person is waiting for (a chat answer, an agent flow tool, a dashboard, the graph evidence shown with an answer) carries a shorter limit of its own, GRAPH_READ_TIMEOUT_MS (15 seconds by default), and FalkorDB answers Query timed out when it is passed. Ingestion and other background work do not carry it and keep the 60s cap above. A read stopped this way is reported as the knowledge graph being unavailable, not as “no data”. See Graph Read Time Limit.
  • Fail-fast on outages — graph calls use a 60s read timeout. While FalkorDB is restarting (it reloads its dataset into memory and answers LOADING during that window), chat and ingestion graph queries fail quickly with a provider-unavailable error instead of hanging. Expect /health/ to report status: degraded until the reload completes, then queries recover automatically — no server restart is needed. A chat answer that meets such a failure says the knowledge graph could not be queried, instead of reporting no data.

FalkorDB read indexes

On FalkorDB, optional indexes on Document(file_id) and on name (one for every node type that has a name) speed up ingestion’s document lookups and the matching of entity names. The gain grows with the size of the graph. They are never built automatically: building one waits for every read in flight on the graph, and the whole graph server waits with it. Build them on purpose, in a maintenance window with ingestion paused and chat quiet, with Force ensure on the Read performance indexes check or with python manage.py ensure_read_indexes --force. On a new instance, build them before the first ingestion, while the graph is empty and the build takes moments. The procedure, the output to expect and the rollback note are in Startup Health. To see which of them are built, building or missing, use View indexes on that row (how). Neo4j does not use these indexes.

Omnistrate / cluster deployment

Both Neo4j and FalkorDB run as internal cluster services with manual scale-to-zero. They are independent of platform boot — server, workers, and initdb no longer wait for a graph DB to be healthy.

Default state

Seeded connection defaults (via initdb → seed_config):
  • FALKOR_URI: redis://falkordb:6379 (cluster internal DNS)
  • NEO4J_URI: neo4j://neo4j:7687
Passwords come from Omnistrate secrets neo4jPassword and falkordbPassword. See Omnistrate deployment.

Controlling graph DB replicas

Use Django admin ServiceConfiguration (same UI as JupyterLab scale-to-zero):
  • keep_alive — maintain 1 replica (use for the active provider)
  • keep_down — maintain 0 replicas (use for the inactive provider to save cost)
  • auto — manual strategy defaults to 1 replica; prefer explicit modes above
Changes are applied by the scale-to-zero sidecar on the server pod (polls every ~60s). Allow a few minutes after toggling before expecting connectivity.

FalkorDB deployment parameters

These are parameters of the Omnistrate deployment. Set them when you create or update the instance. The FalkorDB image is pinned to release 6.0.3 by its tag, falkordb/falkordb:6.0.3, and that tag is the only image the deployment accepts. The deployment names the tag and not a digest because Omnistrate does not keep a digest-only image reference. The image is never the floating latest tag: FalkorDB publishes two engines, a 4.x line and a 6.x line, and latest follows the newest release of either, so on 28 October 2026 it moved from 6.0.3 to 4.22.3. Local development pins the same release by its tag and its digest. Moving to another 6.x release is a deliberate change to the deployment. There is no way back to 4.x once 6.0.3 has saved a graph: read “Upgrading an existing instance to FalkorDB 6.0.3” and “There is no rollback to FalkorDB 4.x” below first. The last five parameters in the table (FalkorDB Query Threads, FalkorDB Query Memory Cap, FalkorDB Max Queued Queries, FalkorDB Output Buffer Limit and FalkorDB Max Memory) are the FalkorDB limit parameters. The container passes them to FalkorDB through FALKORDB_ARGS, so an instance applies them at its next upgrade. Their defaults are sized for the default FalkorDB memory limit of 3 GiB. Earlier releases ran with a 1 GiB query memory cap, no queue limit of their own (FalkorDB’s default applied) and no output buffer limit, so the upgrade lowers those three limits on an instance that does not set its own values.
An instance on a larger FalkorDB node must set these parameters in the same upgrade, or its FalkorDB runs with the limits of the default node. The FalkorDB node follows Medium Instance (mediumInstanceAws, mediumInstanceGcp, mediumInstanceAzure), the instance type that PostgreSQL, Neo4j and RabbitMQ share, so raising it resizes all four. Keep FalkorDB Query Threads times FalkorDB Query Memory Cap at or below 35 percent of the FalkorDB memory limit.

Upgrading an existing instance to FalkorDB 6.0.3

Earlier releases ran the floating latest image, so an instance ran whichever FalkorDB release that tag pointed to when its FalkorDB pod last started: a 4.22 release or a 6.0 release. This release pins FalkorDB 6.0.3, a rewrite of the engine in Rust, by its release tag. An instance moves to it at its next upgrade, and no parameter has to be set.
There is no rollback to FalkorDB 4.x after this upgrade. A 4.x engine opens a graph that FalkorDB 6.0.3 has saved without any error, and once 6.0.3 has created nodes past a multiple of 16,384 it can silently lose every traversal, while node counts, indexes, vector search and the plain relationship count still look right. A live backup with redis-cli --rdb fails on such an engine. If that happens, go forward: put the pinned 6.0.3 image back and every relationship comes back. The details are in There is no rollback to FalkorDB 4.x.Take a backup of the graph before the upgrade and keep it. Once FalkorDB 6.0.3 has saved a graph, a backup that 4.22.1 wrote before 6.0.3 first started is the only way back to 4.22.1. Graph writes made after the upgrade are lost in that rollback, and the files ingested since have to be ingested again.
  • Take the backup first, and prefer a snapshot of the FalkorDB volume. A snapshot is taken from outside the FalkorDB process, so it does not fork FalkorDB and needs no extra FalkorDB memory. Pause ingestion and every other writer first. A snapshot holds the data file as FalkorDB last saved it, so before you take it check that INFO persistence (run with redis-cli and the FalkorDB password) shows rdb_changes_since_last_save:0, which means that nothing is waiting to be saved. If it shows more than 0, keep the writers paused until FalkorDB’s next scheduled save has run and it shows 0.
  • If you copy the data with redis-cli --rdb <file> and the FalkorDB password instead, pause ingestion and every other writer first, and make sure the FalkorDB node has free memory. The copy forks the FalkorDB process, and the extra memory the fork needs grows with the writes made while it runs, so a node that is already close to its memory limit (3 GiB on the default node) can run out of memory. Keep the copy off the FalkorDB volume, because FalkorDB 6.0.3 overwrites dump.rdb on its save schedule and when it shuts down.
  • Your graphs come back. On its first start FalkorDB 6.0.3 loads the data file that 4.22.1 wrote. In our tests a graph of about 354,000 nodes and 436,000 edges came back with the same node and edge content and all 17 indexes operational. It loaded in 4 to 5 seconds, against 15 to 18 seconds on 4.22.1. Do not delete the FalkorDB volume.
  • Expect about a third more FalkorDB memory. On that graph FalkorDB used 1,067 MiB after loading, against 803 MiB on 4.22.1 (33 percent more), and the container used 1,076 MiB against 738 MiB. The peak while loading, which includes the page cache of the data file, was 1,293 MiB against 928 MiB (39 percent more). The worst case we measured, two queries at the 512 MiB query memory cap plus a save, used 2,013 MiB of the 3,072 MiB limit (66 percent), with about 1 GiB free. Multiply the memory your FalkorDB uses today by about 1.4 and compare the result with the memory limit of the FalkorDB node, which is 3 GiB on the default node. By a linear estimate, not a measurement, the largest graph that fits the default node shrinks from about 2.8 times this one to about 1.75 times. If the graph no longer fits, raise the instance type of the medium tier in the same upgrade: the FalkorDB node follows it, and so do PostgreSQL, Neo4j and RabbitMQ, which share that tier.
  • Expect about 37 percent more CPU per query. With 80 simulated users at the same request rate, FalkorDB used about 1.06 of the 2 cores of the default node, against 0.84. The median query latency was 1.8 to 1.9 times that of 4.22.1 and the 95th percentile 33 to 37 percent higher, and throughput stayed at 97 to 98 percent of it.
  • Similarity scores move in the last digits. Vector search returned the same nearest neighbours in our tests. The scores differ from 4.22.1 in the sixth decimal place.
  • Keep the container user. The FalkorDB container runs as user 1001:1001 on the volume the platform provides. Do not start it as root: that start hands the data directory to another user, and a container that runs as 1001:1001 cannot open the directory afterwards.

There is no rollback to FalkorDB 4.x

Do not put the FalkorDB image back to a 4.x release, 4.22.1 and 4.22.3 included. A 4.x engine loads a graph that FalkorDB 6.0.3 has saved without any error, and once 6.0.3 has created nodes past a multiple of 16,384 it can silently lose every traversal. Nothing in the log shows it.
In that state the node data, the node counts, the indexes and vector search still look right, and so does the plain count MATCH ()-[r]->() RETURN count(r), because FalkorDB answers it from its statistics. The same happens when a 6.0.3 graph is copied into a 4.x engine with DUMP and RESTORE. Treat every graph that 6.0.3 has saved as affected. A 4.x engine in this state cannot take a live backup either: redis-cli --rdb fails. If a 4.x engine was started on a graph that 6.0.3 had saved, stop everything that writes to the graph and put the image back to the pinned release 6.0.3. In our tests every edge came back, on 4.22.3 as well as on 4.22.1, from every data file the 4.x engine left behind. Do not start a 4.x engine on that volume again: it stays blind. The only way back to 4.x is a restore, not an image change:
  1. Stop everything that writes to the graph, for example by scaling the job workers to zero.
  2. Restore the backup that 4.22.1 wrote before 6.0.3 first started into the FalkorDB volume while FalkorDB is stopped.
  3. Put the FalkorDB image back to release 4.22.1 and add CACHE_SIZE 1 to the FalkorDB arguments, because 4.22.1 can crash when it evicts a cached query plan at the default size. This is a change to the deployment itself, not a parameter.
  4. Ingest again the files that were ingested after the backup was taken.
After an upgrade or a restore, check the graph with a traversal, not with a count:
  • MATCH (a)-[r]->(b) RETURN max(id(r)) must return a number, not null, when the graph has edges.
  • A join count that you recorded before the change must return the same value afterwards, for example MATCH (a:Label)-[:TYPE]->(b:Label) RETURN count(b) with a label and relationship type of your graph.
Do not use count(r) on its own for this check. FalkorDB answers it from its statistics, so it stays right while traversals are broken. Do not use count(*) for it either: over parallel edges it can count fewer rows on 6.x.

Migration window (both providers up)

Graph migration requires both backends reachable:
  1. Set neo4j and falkordb to keep_alive in ServiceConfiguration
  2. Wait until both pass connectivity checks (Admin → System Settings → provider probe, or inspect /health/ — graph may show checks.neo4j unhealthy until warm)
  3. Run migration from Admin → Settings → Graph migration
  4. Switch GRAPH_PROVIDER in System Settings
  5. Set the inactive backend to keep_down
Run the migration with one server process. Its live progress and the Abort button work only through the server process that runs the job, so with several processes the page can be connected to a process that knows nothing about the job. Before step 3, ask your platform operator to set Server Processes per Pod and Server Minimum Pods to 1 (see Service Scaling), and to raise them again afterwards. Only one migration can run at a time either way.

Health monitoring

GET /health/ reports the active graph provider under checks.neo4j (legacy key name; includes provider: neo4j | falkordb). Graph downtime does not fail the whole endpoint:
  • status: degraded — platform OK, graph unavailable (expected during scale-to-zero)
  • status: unhealthy — Postgres, Redis, or RabbitMQ failed
Chat and graph ingestion need the active provider up; other admin features may work while status is degraded. GET /health/ no longer calls the LLM: checks.llm is always skipped, so a model-provider outage cannot make the endpoint answer 503. The probe moved to GET /health/llm/, which only staff users may call. See Startup Health for the three endpoints.

Schema cache

On server startup a background thread pre-warms the graph schema cache in Redis (24h TTL). If the graph is down at boot, warming fails softly and the first chat query may be slower until cache is populated. With falkordb on keep_alive, this is rarely an issue in steady state. The cache includes relationship fan-out statistics (fan_out_risk, max distinct targets per source, max distinct sources per target) computed from live graph data. After upgrading or switching graph providers, refresh the schema cache so fan-out stats and aggregated property types reflect the active backend. For how relationship attributes interact with graph introspection, see Relationship attributes.

Local development

When using the devcontainer, FalkorDB and its Browser UI are available for ad-hoc Cypher. See FalkorDB Browser (local development).