Add TurboVecHybridAdapter (turbovec POC)#2589
Closed
dev-nid wants to merge 2 commits into
Closed
Conversation
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>
This was referenced Jun 15, 2026
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. |
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. |
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. |
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Third and final slice of the turbovec ANN POC (3-repo coordinated change). Adds a new database adapter to
@qvac/ragthat uses fabric's native vector index for ANN search while keeping HyperDB / Corestore for document rows and replication. Sibling toHyperDBAdapter—HyperDBAdapteris 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 extendingBaseDBAdapter. Documents stored in the existing@rag/documentstable with the unchanged dbSpec (same schema asHyperDBAdapterfor read-path parity). Vectors stored in a single.tvimbinary file atindexPath.@rag/vectors/@rag/centroids/@rag/ivfBucketsare deliberately untouched so a Corestore created byHyperDBAdaptercan be opened byTurboVecHybridAdapterwithout data loss.src/utils/stringIdToU64.js— deterministic SHA-256-prefix mapping from string doc id touint64(little-endian first 8 bytes). Collision math + the loud-fail behavior on duplicates documented inline.TurboVecHybridAdapteris 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-llamacppmodules (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. Coverssave → 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
HyperDBAdaptermeans a Corestore is interchangeable for the read path; you can swap adapters without migrating data.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 freeformmetadataJSON 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.embeddingModelIdoverrides? 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.TurboVecHybridAdapter? Mirrors theIdMapIndexsub-export pattern in embed-llamacpp. Unconditional require would force every@qvac/ragconsumer (includingHyperDBAdapter-only users) to load the native.barebinary — a hard regression.idx.addWithIdsis 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.search? Each top-k slot is an independent HyperDB read. Serial awaits were wasted round-trip latency on synthetic perf measurements;Promise.allbrings it to parity withHyperDBAdapter's shape.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)
.tvimwrite.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_writeshould do the rename dance) so all consumers benefit. Documented in-source.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.reindexfor quantized layouts. Once fabric'sbit_widthbecomes meaningful (real quantization),reindexmay become useful for re-encoding existing vectors at a different precision. Currently a no-op.IdMapIndexC API doesn't expose a filter parameter yet).Verification
test/unit/turbovec-hybrid-adapter.test.js): 25/25 asserts acrossstringIdToU64correctness + adaptersave → search → delete → close → reopen → searchlifecycle.examples/turbovec-poc.js): 6/6 sequence steps pass; deletion survives reopen; top-1 self-match score ≈ 1.0.require('@qvac/rag')loads 0 embed-llamacpp modules; touchingrag.TurboVecHybridAdapterlazily pulls in 4.id-map-index.test.js):require.cachespy confirms the IdMapIndex sub-export never loads the GGMLBert require chain.Findings from
examples/turbovec-vs-hyperdb.jsSynthetic 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.mdagainst real corpora).Companion PRs
#2590
tetherto/qvac-fabric-llm.cpp#153