Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
43ade1b
test(graph): red freeze-boundary suite — post-compaction data loss (P…
TinDang97 Jul 6, 2026
67e80aa
feat(graph): CSR v5 — persist properties, embeddings, edge weights ac…
TinDang97 Jul 6, 2026
ceba165
fix(graph): MergedNodeView — Cypher reads see frozen CSR tier; NodeKe…
TinDang97 Jul 6, 2026
658bb02
fix(graph): cross-tier delta edges + NEIGHBORS/ADDEDGE/PROFILE/replay…
TinDang97 Jul 6, 2026
11ac825
fix(graph): GRAPH.HYBRID/VSEARCH operate across both tiers — Phase 0 …
TinDang97 Jul 6, 2026
8c57d67
perf(graph): per-segment property indexes (lazy, freeze-time data); d…
TinDang97 Jul 6, 2026
2f6e0dc
perf(graph): PhysicalOp::IndexScan — point queries via per-segment pr…
TinDang97 Jul 6, 2026
5ce4883
perf(graph): plan-cache auto-parameterization + LRU + single-parse di…
TinDang97 Jul 6, 2026
252d809
perf(graph): slot-indexed executor rows — kill per-row HashMap alloca…
TinDang97 Jul 6, 2026
aa9fe7d
perf(graph): write-path + traversal allocation cleanups
TinDang97 Jul 6, 2026
38d65cc
perf(graph): CSR row-space BFS fast path — parallel levels + directio…
TinDang97 Jul 6, 2026
fa1abc4
feat(graph): TraversalGuard in Cypher multi-hop ops + real hybrid Hns…
TinDang97 Jul 6, 2026
4fc5190
docs(changelog): graph engine overhaul entry under [Unreleased]
TinDang97 Jul 6, 2026
3a9ab28
fix(graph): stable node/edge ids across WAL replay + replay into writ…
TinDang97 Jul 7, 2026
eed4b08
feat(graph): copy-up writes — SET/DELETE/MERGE on frozen rows (W2-2)
TinDang97 Jul 7, 2026
d21f87a
feat(graph): IndexScan range predicates — WHERE n.p > x prunes via B-…
TinDang97 Jul 7, 2026
fc19284
perf(graph): Cypher Value::String holds Bytes — zero-copy reply path …
TinDang97 Jul 7, 2026
e61d394
refactor(graph): split hybrid.rs into directory module (mod/search/wa…
TinDang97 Jul 7, 2026
60d95a9
perf(graph): HNSW bridge for HYB-02/HYB-04 + off-thread bridge build …
TinDang97 Jul 7, 2026
1b8b861
perf(graph): row-BFS engages on multi-segment frozen graphs (W2-6)
TinDang97 Jul 7, 2026
41822f1
perf(graph): write-side plan cache — repeated write shapes skip parse…
TinDang97 Jul 7, 2026
e1c3191
feat(graph): configurable traversal timeout — per-query TIMEOUT + --g…
TinDang97 Jul 7, 2026
9c5abe0
fix(graph): kill-9 crash suite + Cypher SET/SET-label WAL durability …
TinDang97 Jul 7, 2026
e9c1285
test(graph): frozen-tier BFS criterion benches (W2-10)
TinDang97 Jul 7, 2026
46ce7dd
refactor(graph): delete dead cross-shard traverse scaffolding, docume…
TinDang97 Jul 7, 2026
43cec81
feat(graph): Cypher aggregations — count/sum/avg/min/max/collect with…
TinDang97 Jul 7, 2026
4130f14
feat(graph): OPTIONAL MATCH null-padding + WITH mid-pipeline rebindin…
TinDang97 Jul 7, 2026
4c40c71
chore(graph): wave-2 finalize — CHANGELOG entry + mq_integration conf…
TinDang97 Jul 7, 2026
81a46ff
chore(graph): merge origin/main (squashed PR #231 + waves 3-5) into w…
TinDang97 Jul 7, 2026
a0cdc9a
Merge remote-tracking branch 'origin/main' into feat/graph-engine-wave2
TinDang97 Jul 7, 2026
1bbaec6
fix(graph): v3 disk-offload graph durability — dedicated WAL replay p…
TinDang97 Jul 7, 2026
2cf5069
docs(benchmark): graph wave-2 criterion microbenchmarks (§11.6)
TinDang97 Jul 7, 2026
6745bf4
docs(benchmark): fresh wave-2 vs FalkorDB comparison (§11.7)
TinDang97 Jul 7, 2026
60e21b6
docs(benchmark): 8-thread GCloud verify — wave-2 closes the Cypher ga…
TinDang97 Jul 7, 2026
8d5794b
perf(graph): mutable-tier property index for Cypher point lookups (ta…
TinDang97 Jul 7, 2026
9b38f56
feat(graph): Cypher query-result cache with write-gen invalidation (t…
TinDang97 Jul 7, 2026
1abc499
perf(graph): doorkeeper admission for the Cypher result cache
TinDang97 Jul 7, 2026
9d63460
feat(graph): FTS reuse for Cypher text predicates — CONTAINS/STARTS W…
TinDang97 Jul 7, 2026
fdd6314
style(graph): cargo fmt fixup for text-predicates commit
TinDang97 Jul 7, 2026
556c4c3
docs(benchmark): Cypher-2× wave results — P1 index, P2 cache A/B, P3 …
TinDang97 Jul 7, 2026
852d53a
docs(benchmark): Moon vs Qdrant vector comparison + fresh FalkorDB A/…
TinDang97 Jul 7, 2026
e3ae4d8
docs(benchmark): GCE dedicated-core validation of FalkorDB + Qdrant c…
TinDang97 Jul 7, 2026
061c73c
perf(vector): parallel HNSW build + insert-path compaction trigger — …
TinDang97 Jul 7, 2026
e4a34ba
docs(benchmark): GCE re-validation — time-to-index-green now beats Qd…
TinDang97 Jul 7, 2026
22af755
Merge remote-tracking branch 'origin/main' into feat/graph-engine-wave2
TinDang97 Jul 8, 2026
bf6622b
Merge remote-tracking branch 'origin/main' into feat/graph-engine-wave2
TinDang97 Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions BENCHMARK.md
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,106 @@ label scan** — a property index for inline-equality is a legitimate future opt
v3-2 scope. Moon native builds 23–26× faster and native 1-hop edges out FalkorDB Cypher.
Detail: `docs/reviews/2026-06-17/4FEATURE-VERIFIED.md`.

### 11.6 2026-07-07 Graph wave-2 criterion microbenchmarks (OrbStack Linux VM, aarch64)

Wave-2 engine (`feat/graph-engine-wave2`, PR #237): frozen-tier copy-up writes, IndexScan
ranges, row-BFS multi-segment gate, write-side plan cache, Cypher aggregations,
OPTIONAL MATCH/WITH. `-C target-cpu=native`, fat-LTO bench profile, criterion.
VM cores are shared — treat absolutes as indicative, relatives as solid.

| Benchmark | Median | Notes |
|-----------|:------:|-------|
| 1-hop neighbor, CSR (frozen) | **1.07 ns** | vs memgraph (mutable) 113.8 ns — frozen tier ~106× |
| 2-hop BFS, CSR 1K / 10K | 1.01 / 1.10 µs | vs memgraph 3.47 / 3.64 µs (~3.3×) |
| Edge insert (memgraph) | 206 ns | |
| GRAPH.ADDNODE / ADDEDGE dispatch | 223 / 204 ns | ~4.5–4.9M ops/s per-shard ceiling |
| GRAPH.NEIGHBORS dispatch | 429 ns | |
| CSR freeze, 64K edges | 19.5 ms | per-dirty-graph cost of the checkpoint graph snapshot (P0 fix) |
| Cosine similarity 384d / 768d | 31 / 57 ns SIMD | vs scalar 247 / 522 ns (~8–9×, NEON) |
| Row-BFS frozen 1-segment, 10K depth-3 | **658 µs** | vs sequential memgraph BFS 9.44 ms (~14×) |
| Row-BFS frozen 2-segment | 1.65 ms | W2-6 multi-segment gate ≈ 2.5× single-segment |
| Reader BFS mixed-tier / frozen-2seg | 1.83 / 3.40 ms | |
| `ParallelBfs` memgraph 10K depth-3 | 11.8 ms | **slower than sequential 9.44 ms — see note** |

**`ParallelBfs` note:** the memgraph parallel path is a structural net loss and has **zero
production callers** (the query path uses `BoundedBfs`; both share the row-BFS frozen fast
path, which is where parallelism actually pays). Its neighbor collection stays sequential
(`SegmentMergeReader` borrows `!Send` MemGraph), so it parallelizes only visited-set
filtering while paying per-level neighbor-list clones, a full FxHashSet→DashSet visited
rebuild, and raw `thread::scope` spawns per 128-node morsel (~76 spawns/level at a 9.7K
frontier). Candidate cleanup: retire it or fold into `BoundedBfs`.

### 11.7 2026-07-07 wave-2 vs FalkorDB (OrbStack Linux VM, Docker FalkorDB)

`scripts/bench-graph-compare.sh --nodes 5000` (5K nodes, 3K edges), wave-2 engine.
⚠ Sequential redis-cli harness (one process per command) — per-op cost is dominated by
the ~1 ms redis-cli fork on BOTH sides, so this measures single-client latency deltas,
not server throughput; the §11.5 8-thread persistent-connection GCloud run remains the
throughput reference. Ratios:

| Operation | Moon | FalkorDB | Ratio |
|-----------|:----:|:--------:|:-----:|
| Node insert | 880/s | 716/s | **1.2×** |
| Edge insert | 996/s | 580/s | **1.7×** |
| 1-hop query | 938/s | 684/s | **1.3×** |
| 2-hop query | 1,000/s | 724/s | **1.3×** |
| Cypher pattern match | 862/s | 757/s | **1.1×** |

Moon leads every row in this harness — including Cypher, where §11.5 (pre-wave-2,
concurrent harness) trailed ~4×. The two harnesses are not directly comparable
(fork-bound single client compresses server-side deltas); a fresh 8-thread GCloud run
is the right follow-up before claiming the Cypher gap is closed.

### 11.7b 2026-07-07 Cypher-2× wave (P1 mutable property index, P2 result cache, P3 text predicates)

Follow-up to §11.7/§11.8: profiling showed 82.5% of shard CPU in `index_scan_keys`'s
mutable-tier linear scan. Three features landed (commits `8d5794b9`, `9b38f56c`+`1abc4998`,
`9d634607`):

- **P1 — mutable-tier property index**: point queries stop scanning every memgraph node.
Quiet-host measurement (OrbStack, 8-thread, 5K nodes): 25,506 → **29,014 qps**, p50
0.30 → **0.22 ms** (−27%), shard CPU −~70% (`index_scan_keys` GONE from perf profile;
top symbol drops to 3.9% kernel TCP). Same-box FalkorDB ratio 2.44× → **2.78×**.
The win scales with graph size: the bench scans only 5K nodes; at 1M the old path is
~200× more per-query while the index probe stays O(1).
- **P2 — Cypher result cache** (write-gen invalidated, pre-encoded RESP bytes, TinyLFU
doorkeeper): interleaved A/B vs P1-only (noisy shared host, alternating runs):
cache-hostile cycling **−2.4%** (was −21% before the doorkeeper — admission on second
sighting removed the serialize+insert+O(n)-evict miss cost), hot-key repeat **+1.9%**.
Real payoff is expensive reads (a 24 ms full-graph aggregation becomes a byte-copy),
not client-bound point queries.
- **P3 — text predicates via FTS reuse**: `CONTAINS` / `STARTS WITH` / `ENDS WITH` (new
syntax) + `=~` now prune frozen segments through a `SegmentTextIndex` (presence
bitmap — deliberately NOT token-postings: `CONTAINS 'rust'` must match `"trusted"`,
so tokenized pruning would be unsound; residual Filter stays authoritative). BM25
scoring path implemented + tested, Cypher surface syntax deferred.

### 11.8 2026-07-07 wave-2 vs FalkorDB — 8-thread GCloud harness: **Cypher gap CLOSED**

Same harness as §11.5 (graph phase of `gce-4feature-bench.sh`: 5K nodes, 15K edges,
redis-py, 8 threads, 6s per op class), wave-2 engine @ 1bbaec61, same-box FalkorDB
(Docker). ⚠ Instance is **e2-standard-16** (2.2 GHz shared-core Xeon; c2d capacity
exhausted in-zone), much weaker than §11.5's machines — cross-run absolutes are NOT
comparable, but the same-box Moon:FalkorDB ratio is the valid metric.

| metric | Moon (wave-2) | FalkorDB | ratio | §11.5 ratio (pre-wave-2) |
|--------|:-------------:|:--------:|:-----:|:------------------------:|
| cypher_1hop qps (p50) | 3,489 (1.77 ms) | 3,879 (2.02 ms) | **0.90×** | 0.25× |
| cypher_2hop qps (p50) | **4,518** (0.89 ms) | 3,701 (2.11 ms) | **1.22×** | 0.26× |
| build ops/s | **10,321** | 535 | **19×** | 26× |
| cypher_match_rows | 5 | 5 | correct parity | 4 = 4 |
| native_neighbors qps | 3,185 | — | — | — |

The June deficit — FalkorDB's property index vs Moon's filtered label scan — is gone
at this scale: Moon Cypher point queries improved ~3–4× relative to FalkorDB on the
same box (wave-2 write-side plan cache + IndexScan + executor work). Moon now WINS
2-hop at better p50 and ties 1-hop within 10% on the weakest instance class; on equal
§11.5-class hardware the 1-hop tie likely flips too. Caveat: Moon's p99 (18.9 ms
1-hop) trails FalkorDB's (3.1 ms) on this shared-core instance — worth a look on
pinned cores. Interesting inversion: Moon Cypher 1-hop (3,489 qps) now beats its own
native GRAPH.NEIGHBORS (3,185 qps) under concurrency — the plan cache amortizes
parsing to near-zero.

---

## 12. Full-Text Search
Expand Down
117 changes: 116 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,122 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
HNSW bridge (heap-owned; mmap-backed sections count 0 per the
`RawF16Store` precedent), keeping the elastic memory budget honest.

### Changed — consolidated dependency bumps (PR #TBD)
### Changed — graph engine wave 2: durability, Cypher coverage, hardening (PR #TBD)

- **Durability (P0, found by GCP production-hardening soak):** graph data now
survives kill -9 under the DEFAULT disk-offload (WAL v3) configuration.
Two stacked pre-existing bugs erased graphs on crash: (A) v3 recovery
collected graph WAL commands into a throwaway replay engine — graph replay
was a complete no-op in v3 mode (a dedicated `replay_graph_wal_v3` boot
pass now applies them); (B) the checkpoint advanced the WAL replay floor
and recycled segments without ever snapshotting the graph store —
`save_graph_store` ran only on graceful shutdown and never persisted the
mutable tier (checkpoint finalize now freezes each graph's write buffer,
persists segments + a `snapshot_lsn` replay floor, and ABORTS the
checkpoint if the graph snapshot fails, keeping the old floor). The prior
crash suite ran exclusively with `--disk-offload disable`; new G4/G5
scenarios cover the default config with and without a checkpoint crossing.
- **Durability:** stable external node/edge ids across WAL replay (handles
handed to clients before a crash resolve to the same rows after recovery);
Cypher `SET` property/label writes now emit WAL records
(`GRAPH.SETPROP`/`GRAPH.SETLABEL` — previously a documented durability gap:
SET mutations silently vanished on kill -9); new kill-9 crash-recovery
suite (`tests/crash_recovery_graph_durability.rs`: mutable tier, frozen
64K-edge tier rebuilt through replay-time freeze, double-crash idempotence).
- **Cypher coverage:** aggregations `count`/`sum`/`avg`/`min`/`max`/`collect`
with implicit grouping, `DISTINCT`, and count-over-zero-rows semantics;
`OPTIONAL MATCH` null-pads unmatched expansions (previously compiled
silently to inner MATCH; unsupported shapes now reject loudly);
`WITH` rebinds the pipeline mid-query (aggregate + `WHERE`-as-HAVING,
`ORDER BY`/`SKIP`/`LIMIT` between WITH and RETURN; previously every clause
after WITH ran on an empty row stream). `WITH *` rejects loudly.
- **Copy-up writes:** `SET`/`DELETE`/`MERGE` on frozen rows copy the row up
into the write buffer instead of silently missing the frozen tier.
- **Query performance:** IndexScan range predicates (`WHERE n.p > x` prunes
via per-segment B-tree-ish numeric index, superset semantics + residual
filter); write-side plan cache (repeated write shapes skip parse+compile);
row-BFS fast path engages on multi-segment fully-frozen graphs (~4× vs
reader fallback, criterion-pinned); `Value::String` holds `Bytes` for a
zero-copy reply path; HYB-02/HYB-04 use the HNSW bridge with off-thread
bridge builds.
- **Query performance — mutable-tier property index (task #31):** `MATCH
(a:N {id: X})` on the write buffer used to degrade to a full
`O(live_node_count)` linear scan even though `PhysicalOp::IndexScan` was
already the plan (profiling on the 5K-node/15K-edge Cypher point-query
bench put this scan at 82.5% of shard CPU — the mutable tier never
freezes below `edge_threshold`, default 64,000). A new incrementally
maintained `MutablePropertyIndex` (`src/graph/index.rs`, mirrors the
frozen tier's `SegmentPropertyIndexes` numeric-BTree/string-hash split,
keyed by `NodeKey`) turns the mutable-tail probe into an O(log N +
|result|) index seed; `index_scan_keys`'s residual label/MVCC/property
checks are unchanged (SUPERSET contract preserved, no planner changes).
`MemGraph::set_node_property`/`remove_node_property`/`undelete_node` are
now the single source of truth for node-property mutation, replacing 5
previously hand-rolled call sites (Cypher `SET`, MERGE ON CREATE/MATCH
SET, TXN.ABORT undo, WAL replay) that could silently let the index drift
from live state. Closes a TXN.ABORT `UndeleteNode` gap found during
design review: undoing a `DELETE` inside a transaction now re-indexes the
restored node instead of leaving it live but permanently unreachable by
index probes.
- **Query performance — Cypher result cache (task #32):** read-only
`GRAPH.QUERY`/`GRAPH.RO_QUERY` now cache the fully-encoded RESP reply
bytes for repeated identical queries (same raw Cypher text + args),
keyed by `(query_hash, args_hash)` and served via the existing
`Frame::PreSerialized` passthrough — no new protocol variant needed, and
both RESP2 and RESP3 encodings are cached in separate slots so a
protocol-version switch is a clean miss rather than a stale re-encode.
Invalidation is a per-graph monotonic `write_gen: u64` bumped by
`NamedGraph::touch()` at every real mutation site (plain `GRAPH.ADDNODE`/
`GRAPH.ADDEDGE`, the Cypher write-plan mutation loop, `TS.*` temporal
invalidation, and TXN.ABORT rollback of graph undo-ops) — deliberately
NOT at `freeze_and_compact`, which reshapes storage without changing
query-visible content. The write_gen is captured before executing a
read and the encoded reply is only cached if it is still unchanged
after — a miss is always safe (SUPERSET semantics), so a benign race
just skips caching rather than serving stale data. Decayed queries and
errored/timed-out results are never cached. `ResultCache` is a bounded
per-graph LRU (256 entries / 4MiB default, `src/graph/cypher/
result_cache.rs`) whose resident bytes are folded into
`GraphStore::resident_bytes()`; dropping a graph (`GRAPH.DELETE`) drops
its cache with it. The cache is only consulted on the connection-local
dispatch path where the negotiated protocol version is reliably known
(`dispatch_graph_read`, threaded as `Option<u8>`); the cross-shard
`GraphCommand` hop and `graph_query_or_write`'s internal read-execute
calls pass `None` and bypass the cache entirely rather than risk an
unverified protocol version.
- **Query performance — FTS reuse for Cypher text predicates (task #33):**
first-class `CONTAINS`/`STARTS WITH`/`ENDS WITH` Cypher operators (pure
syntax sugar over the existing `=~` byte-level checks — `src/graph/
cypher/lexer.rs`, `ast.rs`, `parser/expr.rs`, `executor/eval.rs`), plus a
new per-frozen-segment `SegmentTextIndex` (`src/graph/text_index.rs`,
lazy `OnceLock` + `resident_bytes` accounting, same pattern as
`SegmentPropertyIndexes`/`hnsw_bridge`) that accelerates `CONTAINS`/
`STARTS WITH`/`ENDS WITH`/`=~` conjuncts in `WHERE` (`planner.rs`'s new
`extract_text_conjuncts`, mirroring W2-3's `extract_range_conjuncts`).
Reuses `crate::text::posting::PostingStore`/`crate::text::bm25::
{FieldStats, bm25_score}`/`crate::text::term_dict::TermDictionary`
verbatim (already ID-space-agnostic, zero-fork). **Correctness note:**
the index prunes on PROPERTY PRESENCE only, not token identity — a
tokenized/stemmed/case-folded posting lookup cannot safely decide
substring/prefix/suffix containment (e.g. `CONTAINS 'rust'` must also
match `"trusted"`, which tokenizes to a different term entirely), so
`candidate_rows` returns every row whose target property is a
String/Bytes at all, and the existing residual `Filter` remains the sole
decider (SUPERSET contract, same as `prop_eq`/`prop_range`). The
MUTABLE tier gets no acceleration (falls back to an exact scan of the
write buffer, pre-approved scope decision — mirrors the pre-task-#31
numeric story); a `GraphUnion`-merged segment does not remap posting row
ids and simply rebuilds its text index lazily on first use post-merge.
`SegmentTextIndex::bm25_score_for` is a tested-but-unwired internal BM25
relevance-scoring hook (no Cypher `ORDER BY` grammar was specified for
it) — proves the `bm25_score` reuse end-to-end; surface syntax is a
documented follow-up.
- **Operability:** traversal timeout is configurable — `--graph-timeout-ms`
server default plus per-query `GRAPH.QUERY ... TIMEOUT <ms>` (RedisGraph
parity, 0 = unlimited).
- **Dead code:** cross-shard traverse scaffolding deleted (532 lines, no
sender existed); the single-shard-per-graph sharding model is now
documented in `src/graph/mod.rs`.

- Cargo: `ringbuf` 0.4.8 → 0.5.0 and `metrics-exporter-prometheus` 0.16.2 →
0.18.3 (semver-major; both compile and test green with no code changes —
Expand Down
Loading
Loading