zlf is a RocksDB-backed graph database with a single WAM Prolog runtime. Its primary interfaces are:
zlfzlf serve [port]zlf repl [db_path]git clone <repository-url>
cd zlf
cargo build --release
The executable is target/release/zlf.
The configuration is loaded from ./zlf.json, then ~/.zlf/config.json, with environment overrides applied last. The default is:
{
"db_path": "./zlf-db",
"embedding": {
"enabled": false,
"index_engine": "exact",
"provider": "ollama",
"api_endpoint": "http://localhost:11434",
"api_key": null,
"model": "bge-m3:latest",
"dimension": 1024
}
}
Embedding is deliberately disabled by default. Enable it only for workloads that benefit from semantic retrieval:
{
"db_path": "./zlf-db",
"embedding": {
"enabled": true,
"index_engine": "hnsw",
"provider": "ollama",
"api_endpoint": "http://localhost:11434",
"model": "bge-m3:latest",
"dimension": 1024
}
}
index_engine is exact or hnsw. HNSW always retains exact RocksDB as source of truth and fallback.
Environment overrides:
ZLF_DB_PATH=./zlf-db
ZLF_EMBED_ENABLED=true
ZLF_VECTOR_INDEX_ENGINE=hnsw
ZLF_EMBED_PROVIDER=ollama
ZLF_EMBED_ENDPOINT=http://localhost:11434
OLLAMA_ENDPOINT=http://localhost:11434
ZLF_EMBED_MODEL=bge-m3:latest
ZLF_EMBED_DIMENSION=1024
ZLF_EMBED_API_KEY=...
Start or reopen a database:
target/release/zlf repl ./example-db
The REPL accepts facts, rules, directives, and queries:
node(alice, [person], { name: "Alice", title: "Engineer" }).
node(bob, [person], { name: "Bob", title: "Engineer" }).
knows(alice, bob).
friend(X, Y) :- person(X), person(Y), knows(X, Y).
? person(X).
? property(alice, name, Name).
? friend(alice, Who).
Exit with :quit, :exit, or Ctrl-D. Use :help for examples. A successful ground query has no variable bindings and is printed as [{}].
Facts are persisted through the canonical storage writer:
node(alice).
node(alice, [person, employee], { name: "Alice", age: 30 }).
person(alice).
knows(alice, bob).
property(alice, title, "Staff Engineer").
Rules are compiled and persisted through StorageRuleStore:
colleague(X, Y) :- works_at(X, C), works_at(Y, C), X \= Y.
reachable(X, Y) :- knows(X, Y).
reachable(X, Y) :- knows(X, Z), reachable(Z, Y).
Queries start with ?:
? node(Id).
? person(Id).
? property(Id, name, Name).
? edge(Source, Type, Target).
? knows(alice, Who).
? prop_name(Id, Name).
A line can contain multiple facts:
node(a). node(b). follows(a, b).
node(Id).
label(Id, Label).
property(Id, Key, Value).
edge(Source, Type, Target).
Shortcuts are derived from labels, edge types, and properties:
person(alice). % label shortcut
knows(alice, bob). % edge-type shortcut
prop_name(alice, Name). % property shortcut
? retract(person(alice)).
? retract(edge(alice, knows, bob)).
? retract(prop_title(alice, _)).
Writing Label(Id) to an existing node merges the label and preserves existing properties.
? bm25("software engineer", Node, Score).
? vector_similar(alice, Node, Score).
? temporal_on("2026-07-15", Node).
? temporal_between("2026-07-01", "2026-08-01", Node).
? valid_at("2026-07-15T00:00:00Z", Node).
? valid_overlaps("2026-07-01T00:00:00Z", "2026-08-01T00:00:00Z", Node).
vector_similar/3 returns an explicit index-unavailable error when embedding is disabled. BM25, vector, and temporal indexes only contain fields selected by an active IndexProfile.
A BM25 profile can be created and activated directly in the REPL:
:- index_profile(knowledge, 1, {
matcher: { node_labels: { labels: [document] } },
fields: {
body: {
bm25: {
analyzer_id: "unicode_jieba_v1",
analyzer_version: 1,
weight: 1.0,
k1: 1.2,
b: 0.75
}
}
}
}).
:- activate_index_profile(knowledge, 1).
Profiles are immutable by (name, version). Change the version instead of modifying an existing artifact.
An IndexProfileArtifact selects entities and fields and declares which derivative indexes each field feeds.
| Field | Meaning |
|---|---|
schema_version |
Currently 1 |
name / version |
Immutable profile identity |
source_hash |
Canonical profile hash; the CLI accepts "" and computes it |
matcher |
node_labels or edge_types |
fields |
Map from property name to BM25/vector/temporal options |
created_at |
RFC 3339 timestamp |
Matcher forms:
{"node_labels": {"labels": ["document"]}}
{"edge_types": {"edge_types": ["mentions"]}}
The current Tantivy contract pins k1=1.2, b=0.75, analyzer ID unicode_jieba_v1, and analyzer version 1. weight must be positive.
{
"bm25": {
"analyzer_id": "unicode_jieba_v1",
"language": "en",
"analyzer_version": 1,
"weight": 1.0,
"k1": 1.2,
"b": 0.75
}
}
Vector fields require embedding.enabled=true:
{
"vector": {
"model_profile": "bge_m3_dense_v1",
"chunking": {"whole_field": {"version": 1}}
}
}
Supported chunking forms:
{"explicit": {"version": 1}}
{"whole_field": {"version": 1}}
{"paragraph_heading": {"version": 1}}
{"fixed_token_window": {"version": 1, "size": 128, "overlap": 16}}
A field may be an event instant, validity start, or validity end:
{"temporal": "event"}
{"temporal": "valid_from"}
{"temporal": "valid_to"}
At most one valid_from and one valid_to field are allowed; valid_to requires valid_from.
{
"schema_version": 1,
"name": "knowledge",
"version": 1,
"source_hash": "",
"matcher": {"node_labels": {"labels": ["document"]}},
"fields": {
"title": {
"bm25": {
"analyzer_id": "unicode_jieba_v1",
"analyzer_version": 1,
"weight": 2.0,
"k1": 1.2,
"b": 0.75
}
},
"body": {
"bm25": {
"analyzer_id": "unicode_jieba_v1",
"analyzer_version": 1,
"weight": 1.0,
"k1": 1.2,
"b": 0.75
}
},
"published_at": {"temporal": "event"}
},
"created_at": "2026-07-15T00:00:00Z"
}
Install, activate, list, and inspect lifecycle status through JSON-over-STDIO:
{"command":"put_index_profile","path":"./zlf-db","profile":{...}}
{"command":"activate_index_profile","path":"./zlf-db","name":"knowledge","version":1}
{"command":"list_index_profiles","path":"./zlf-db"}
{"command":"index_status","path":"./zlf-db","target":"bm25"}
{"command":"wait_indexes","path":"./zlf-db","targets":["bm25"],"minimum_sequence":1,"timeout_ms":1000}
Activation rebuilds and publishes the relevant generation. A profile containing vector fields is rejected while embedding is disabled.
Send one JSON request per line to target/release/zlf. Responses are also one JSON object per line.
echo '{"command":"init","path":"./zlf-db"}' | target/release/zlf
Current commands:
| Command | Purpose |
|---|---|
init |
Initialize a database |
add_node, get_node |
Create/read nodes |
add_edge, get_edge, edge_ids |
Create/read/resolve edges |
patch_node_properties, set_node_property, remove_node_property |
Mutate node properties |
patch_edge_properties, set_edge_property, remove_edge_property |
Mutate edge properties |
query |
Execute a Prolog query/directive/rule |
search |
BM25 text search |
put_index_profile, activate_index_profile, list_index_profiles |
Manage profiles |
index_status, wait_indexes |
Observe index lifecycle |
vector_index_status, rebuild_vector_index |
Observe/request optional HNSW publication |
import, export |
JSON graph import/export |
embed |
Generate an embedding when enabled |
config |
Read/write zlf.json configuration |
Examples:
echo '{"command":"add_node","path":"./zlf-db","labels":["person"],"properties":{"name":"Alice"}}' | target/release/zlf
echo '{"command":"query","path":"./zlf-db","query":"? property(Id, name, Name)."}' | target/release/zlf
echo '{"command":"search","path":"./zlf-db","query":"software engineer"}' | target/release/zlf
Success and error envelopes:
{"type":"success","data":{}}
{"type":"error","code":"QUERY_FAILED","message":"..."}
target/release/zlf serve 8520
POST /api — the same JSON command protocolPOST /api/sse — SSE query responsesGET /health — health checkINDEX_UNAVAILABLE or an Index 'vector_embedding' ... message means embedding/vector indexing is disabled for the requested operation.echo '{"command":"rebuild_vector_index","path":"./zlf-db"}' | target/release/zlf
echo '{"command":"vector_index_status","path":"./zlf-db"}' | target/release/zlf