Skip to content

Add TurboVecHybridAdapter (turbovec POC)#2589

Closed
dev-nid wants to merge 2 commits into
tetherto:mainfrom
dev-nid:poc/turbovec-rag
Closed

Add TurboVecHybridAdapter (turbovec POC)#2589
dev-nid wants to merge 2 commits into
tetherto:mainfrom
dev-nid:poc/turbovec-rag

Conversation

@dev-nid

@dev-nid dev-nid commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Context

Third and final slice of the turbovec ANN POC (3-repo coordinated change). Adds a new database adapter to @qvac/rag that uses fabric's native vector index for ANN search while keeping HyperDB / Corestore for document rows and replication. Sibling to HyperDBAdapterHyperDBAdapter is not modified, existing users keep working unchanged.
Depends on the IdMapIndex JS class from the companion embed-llamacpp PR (sub-export @qvac/embed-llamacpp/idMapIndex).

What was done

  • src/adapters/database/TurboVecHybridAdapter.js — hybrid adapter extending BaseDBAdapter. Documents stored in the existing @rag/documents table with the unchanged dbSpec (same schema as HyperDBAdapter for read-path parity). Vectors stored in a single .tvim binary file at indexPath. @rag/vectors / @rag/centroids / @rag/ivfBuckets are deliberately untouched so a Corestore created by HyperDBAdapter can be opened by TurboVecHybridAdapter without data loss.
  • src/utils/stringIdToU64.js — deterministic SHA-256-prefix mapping from string doc id to uint64 (little-endian first 8 bytes). Collision math + the loud-fail behavior on duplicates documented inline.
  • Export wiring: TurboVecHybridAdapter is exposed two ways: a lazy property getter on the main entry (require('@qvac/rag').TurboVecHybridAdapter), and a dedicated sub-export @qvac/rag/turbovec. Both resolve to the same class. Lazy load is load-bearing: require('@qvac/rag') alone now loads zero @qvac/embed-llamacpp modules (verified 0 → 4 on first property touch). HyperDBAdapter-only consumers retain the zero-native-dep status quo.
  • test/unit/turbovec-hybrid-adapter.test.js — brittle-bare test against a temporary Corestore. Covers save → search → delete → close → reopen → search; verifies reverse-map rebuild from doc metadata, content/metadata round-trip, and self-match scores. 25 assertions.
  • examples/turbovec-poc.js — end-to-end smoke that synthesizes (or loads from the bench harness, if present) 200 × 1024 vectors and runs the full sequence: ingest, search, delete, persist + reopen.
  • examples/turbovec-perf.js + examples/turbovec-vs-hyperdb.js — synthetic-corpus perf probes. See the "Findings" section below.

Why these choices

  • Why hybrid (HyperDB for docs, separate .tvim for vectors)? Preserves replication-over-hyperswarm for the document corpus (HyperDB strength) while moving the ANN search to a format that's actually fit for it. Keeping the document schema identical to HyperDBAdapter means a Corestore is interchangeable for the read path; you can swap adapters without migrating data.
  • Why a reverse map (metadata.__turbovec_u64Id) inside the doc rows? We can't extend the existing dbSpec without regenerating it (per prompt direction); we need a u64 → string-id lookup for search-result resolution; stashing the hash in the freeform metadata JSON is the smallest-blast-radius option (16 B/doc memory, rebuilt at open). Bigint doesn't survive JSON so the value is stored as a base-10 string.
  • Why reject per-doc embeddingModelId overrides? The adapter is bound to one model at construction. Earlier draft warned + silently picked one of them, which produced contradictory behavior. Now mixed-model batches are rejected at validation — silent picking would cause hard-to-debug recall problems downstream.
  • Why a lazy getter + sub-export for TurboVecHybridAdapter? Mirrors the IdMapIndex sub-export pattern in embed-llamacpp. Unconditional require would force every @qvac/rag consumer (including HyperDBAdapter-only users) to load the native .bare binary — a hard regression.
  • Why atomic add + rollback? idx.addWithIds is atomic; we do the native insert first, then HyperDB writes. If the HyperDB transaction fails we remove the freshly-added u64s from the native index so the two stores stay consistent.
  • Why a parallelized search? Each top-k slot is an independent HyperDB read. Serial awaits were wasted round-trip latency on synthetic perf measurements; Promise.all brings it to parity with HyperDBAdapter's shape.
  • Why no reindex? The native index is data-oblivious — no centroid clustering, no IVF buckets — so reindexing has no meaning. Returns { reindexed: false, reason: 'data-oblivious; no rebuild needed' }.

What's left (out of POC scope)

  • Atomic .tvim write. idx.write() is full-rewrite, not write-to-tmp + rename. A crash mid-write leaves a truncated file. Fix belongs in fabric (ggml_vec_index_write should do the rename dance) so all consumers benefit. Documented in-source.
  • Cross-store atomicity. tx.flush() + idx.write() are not transactional relative to each other. A crash between them leaves HyperDB one batch ahead of the index. Fix belongs in fabric (journaled / delta-log .tvim format). Documented in-source.
  • reindex for quantized layouts. Once fabric's bit_width becomes meaningful (real quantization), reindex may become useful for re-encoding existing vectors at a different precision. Currently a no-op.
  • Filtered search. Not in POC scope (and the IdMapIndex C API doesn't expose a filter parameter yet).

Verification

  • Unit test (test/unit/turbovec-hybrid-adapter.test.js): 25/25 asserts across stringIdToU64 correctness + adapter save → search → delete → close → reopen → search lifecycle.
  • End-to-end smoke (examples/turbovec-poc.js): 6/6 sequence steps pass; deletion survives reopen; top-1 self-match score ≈ 1.0.
  • Lazy-load invariant (verified at the REPL): require('@qvac/rag') loads 0 embed-llamacpp modules; touching rag.TurboVecHybridAdapter lazily pulls in 4.
  • Lifecycle isolation (covered authoritatively in the embed-llamacpp PR's id-map-index.test.js): require.cache spy confirms the IdMapIndex sub-export never loads the GGMLBert require chain.

Findings from examples/turbovec-vs-hyperdb.js

Synthetic 1024-dim corpus, top-k=10, Apple Silicon macOS. Recall not measured (no ground truth on synthetic data; recall comparison lives in benchmarks/turbovec-eval/REPORT.md against real corpora).

N Adapter Queryable Ingest RSS Query mean
3,633 TurboVecHybridAdapter (POC scalar f32) 3,633 / 3,633 142 ms 263 MB 1.69 ms
3,633 HyperDBAdapter (retain-full) 3,633 / 3,633 680 ms 507 MB 25.9 ms
10,000 TurboVecHybridAdapter 10,000 / 10,000 367 ms 490 MB 4.44 ms
10,000 HyperDBAdapter (retain-full) 10,000 / 10,000 2.0 s 794 MB 118.5 ms
50,000 TurboVecHybridAdapter 50,000 / 50,000 2.5 s 1,430 MB 21.7 ms
50,000 HyperDBAdapter (retain-full) 50,000 / 50,000 13.5 s 1,411 MB 1,466 ms
POC scales linearly (~450 ns/vector). At 50k the POC is ~68× faster than well-configured HyperDB on query and ~5× faster on ingest.

Note for future benchmarkers: the vs-hyperdb script writes unique contentHash per row. An earlier draft with identical contentHash values surfaced a real HyperDBAdapter issue — its internal _filterDuplicates collapses the entire batch to a single survivor, producing misleadingly fast benchmark numbers because nothing is actually indexed.

Companion PRs

#2590
tetherto/qvac-fabric-llm.cpp#153


dev-nid and others added 2 commits June 15, 2026 17:46
Adds the embed-llamacpp slice of the turbovec POC: a new Bare-addon JS
class `IdMapIndex` bound to fabric's `ggml-vector-index` C API. Sibling
to `GGMLBert`; the same .bare binary carries both surfaces. The C API
itself ships in the companion fabric PR
(dev-nid/qvac-fabric-llm.cpp#poc/turbovec-embed).

  - addon/src/model-interface/VectorIndex.{hpp,cpp}: RAII C++ wrapper
    around `ggml_vec_index_t*`. Marked `noexcept` throughout; const
    methods where the C API allows.
  - addon/src/addon/VectorIndexErrors.hpp: maps fabric error codes to
    JS error names; mirrors the BertErrors pattern.
  - addon/src/js-interface/vector-index-binding.cpp: N-API surface
    (idx_create / load / add / search / remove / contains / prepare /
    write / len / dim / bit_width). Free functions registered onto the
    addon's exports next to the existing BertInterface plumbing.
    Deliberately depends only on the VectorIndex wrapper and fabric's
    C API — no `BertModel` / `LlamaLazyInitializeBackend` symbols, so
    the binding's load does not boot fabric's LLM backend.
  - idMapIndex.js: thin JS class over the bindings. Float32Array
    vectors, BigUint64Array external ids. `addWithIds` is atomic
    (rejects on duplicate u64). `dispose()` is honest about being a
    reference-drop; the native handle is reclaimed at GC via the JS
    external's finalizer.
  - index.js + package.json: re-export IdMapIndex as a named property
    on the main entry (discoverability) and as a dedicated sub-export
    `@qvac/embed-llamacpp/idMapIndex` (lifecycle isolation — consumers
    can opt in without loading the GGMLBert require chain).
  - test/integration/id-map-index.test.js: exercises every JS method
    end-to-end (no model needed); includes a runtime spy on
    `require.cache` that asserts importing the sub-export does NOT
    load `index.js` (GGMLBert) or `addon.js` (BertInterface). 73
    asserts. Pair-bound with fabric's `tests/test-vector-index.cpp`.
  - vcpkg overlay (vcpkg/ports/qvac-fabric/): pins qvac-fabric to a
    commit on the dev-nid fork via vcpkg_from_github, since the public
    `tetherto/qvac-registry-vcpkg` qvac-fabric port has no 8828.x line
    yet and the manifest pins >=8828.1.0. `QVAC_FABRIC_LOCAL_PATH` env
    var switches the portfile into local-working-tree mode for the
    Phase 0 fast-edit loop. Helper scripts: `scripts/sync-fabric-
    overlay.sh` + `scripts/rebuild-fabric.sh`.
  - CMakeLists.txt: wires the new sources, registers the overlay,
    adds `llama::llama` / `llama::common` aliases when the fabric
    config doesn't already provide them.
  - vcpkg.json: adds top-level `name` + `version-string`. vcpkg
    requires these when the manifest uses version>= constraints with
    overlay-ports (verified empirically); commented inline.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds the @qvac/rag slice of the turbovec POC: a new
`TurboVecHybridAdapter` that extends `BaseDBAdapter` and uses
`IdMapIndex` from `@qvac/embed-llamacpp` (sibling PR / earlier commit
on this branch) for ANN search, while keeping HyperDB / Corestore for
document rows and replication. `HyperDBAdapter` is not modified;
existing users keep working.

  - src/adapters/database/TurboVecHybridAdapter.js: hybrid adapter.
    Documents stored in `@rag/documents` with the unchanged dbSpec
    (same schema as HyperDBAdapter for read-path parity); vectors
    stored in a single `.tvim` binary at `indexPath`. The `@rag/vectors`
    / `@rag/centroids` / `@rag/ivfBuckets` tables are deliberately
    untouched so a corestore created by HyperDBAdapter can be opened
    by this adapter without data loss. A bigint→string-id reverse map
    is held in memory only, rebuilt at open by scanning each doc's
    `metadata.__turbovec_u64Id` (stashed as a base-10 string because
    bigint doesn't survive JSON). Per-doc embeddingModelId overrides
    are rejected — the adapter is bound to one model. Atomic add: if
    the native index rejects (duplicate u64) HyperDB stays untouched.
    Failures during the HyperDB write roll back the native-index
    inserts so the two stores stay in sync. `_open` failure path
    nulls every owned reference via a shared `_teardown` helper.
    Persistence durability gaps (non-atomic .tvim write; no
    cross-store atomicity between tx.flush and idx.write) are
    documented inline — the fix belongs in fabric.
  - src/utils/stringIdToU64.js: deterministic SHA-256-prefix mapping
    of string ids to uint64 (little-endian first 8 bytes). Collision
    math + the loud-fail strategy on duplicate hashes documented.
  - index.js + package.json: TurboVecHybridAdapter is exposed via a
    lazy property getter on the main entry AND a dedicated sub-export
    `@qvac/rag/turbovec`. Lazy load is the load-bearing detail:
    `require('@qvac/rag')` alone now loads zero embed-llamacpp modules
    (verified at 0 modules pre-touch, 4 post-touch), preserving the
    zero-native-dep status quo for HyperDBAdapter-only consumers.
  - test/unit/turbovec-hybrid-adapter.test.js: brittle-bare test
    against a temporary Corestore. Covers save → search → delete →
    close → reopen → search; verifies reverse-map rebuild from
    metadata, content/metadata round-trip, and self-match scores.
    25 asserts.
  - examples/turbovec-poc.js: end-to-end smoke script that loads or
    synthesizes 200 × 1024 vectors and runs the full sequence
    (lifecycle isolation, ingest, search, delete, persist + reopen).
    The authoritative lifecycle-isolation invariant lives in the
    embed-llamacpp integration test (require.cache spy); this script
    just exercises the chain.
  - examples/turbovec-perf.js + examples/turbovec-vs-hyperdb.js:
    synthetic-corpus perf probes. The vs-hyperdb run found a real
    HyperDBAdapter dedup-by-contentHash collapse that surfaces when
    benchmarking with duplicated contentHashes; the script's docs
    generator now writes unique hashes per row so the comparison is
    honest. Both scripts default to N=3633 d=1024 (matching the bench
    harness corpus shape) with overrides for larger N.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

This draft PR is stale because it has been open 21 days and the author has not commented since opening. It is flagged for removal. Remove the stale label or comment on the PR or this will be closed in one day.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

This draft PR was closed because it has been stalled for 22 days with no author comment since opening. You can reopen this PR later if it is still necessary.

@github-actions github-actions Bot closed this Jul 8, 2026
@gianni-cor gianni-cor removed the Stale label Jul 9, 2026
@gianni-cor gianni-cor reopened this Jul 9, 2026
@github-actions github-actions Bot added the Stale label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

This draft PR is stale because it has been open 21 days and the author has not commented since opening. It is flagged for removal. Remove the stale label or comment on the PR or this will be closed in one day.

@github-actions

Copy link
Copy Markdown
Contributor

This draft PR was closed because it has been stalled for 22 days with no author comment since opening. You can reopen this PR later if it is still necessary.

@github-actions github-actions Bot closed this Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants