Skip to content

QVAC-24971 feat[api]: expose the TurboVec vector index on the SDK - #4457

Open
lauripiisang wants to merge 6 commits into
mainfrom
QVAC-24971
Open

lauripiisang wants to merge 6 commits into
mainfrom
QVAC-24971

Conversation

@lauripiisang

@lauripiisang lauripiisang commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

🎯 What problem does this PR solve?

  • Built-in RAG always persists through HyperDB, and ragTurbovec: true only swaps the index on that same store. @qvac/sdk did not expose the native TurboVec index, so apps could not use it with a document store of their own.

📝 How does it solve it?

  • New vectorIndex wire method with create/load/add/search/remove/contains/write/dispose operations. The worker holds each open index in memory and persists it only when the app calls write(), which snapshots to a path the app chooses; loadVectorIndex() reopens that snapshot. dispose() and worker cleanup release memory only.
  • The worker registry does only bookkeeping (ids, row validation, snapshot paths, lifetime) against an inference-owned VectorIndexBackend interface. The one TurboVec-specific file, plugins/turbovec-backend.ts, adapts the existing turbovecIndexProvider plugin capability to that interface and owns the warm-up call, the result padding sentinel, and storage-mode reporting. All five VectorIndexStorage modes are served by that backend; the two TurboVec modes add the dimension rule (multiple of 8, at most 1024).
  • createVectorIndex() and loadVectorIndex() return a handle object instead of eight flat functions. Ids travel as decimal uint64 strings or safe integers and vectors as number rows, so the generated Python client works with no hand-written helpers. VectorIndexStorage is a name-to-value constant registered in the contract, like ModelType.
  • Relative snapshot paths resolve under the QVAC data directory; absolute paths are used as given. New VECTOR_INDEX_* error codes.
  • VectorIndex.storage is known after createVectorIndex. After loadVectorIndex it is derived from the snapshot's bit width, so 4-bit snapshots report undefined because q4 and turbovec-q4 share a width. dispose() marks the handle disposed only once the worker confirms, so a rejected dispose can be retried instead of leaking the index until shutdown.
  • Regenerated packages/sdk/contract and the Python _generated client. New examples/rag/rag-turbovec.ts and a RAG guide section that keeps documents in the app's own store.
  • Library-only: no qvac serve route. Raw index handles do not map onto the OpenAI vector-store API, and /vector_stores already uses TurboVec through ragTurbovec.

🧪 How was it tested?

  • Inference brittle tests through dispatch with the provider fixture (including padding stripping), SDK unit test for the handle over a fake transport, contract and Python generation checks, docs example check, and the rag-turbovec example run end to end.
  • Desktop e2e: vector-index-* (three tests, one smoke) and the full rag-* category (10 tests) as regression.
  • All five VectorIndexStorage modes exercised on the real native index: create, add, search, write, reload, search again.

🔌 API Changes

const index = await createVectorIndex({ dim: 1024, storage: VectorIndexStorage.TURBOVEC_Q4 })
await index.add({ ids: ['1', '2'], vectors: embeddings })
const hits = await index.search({ query: queryEmbedding, k: 3 }) // [{ id, score }]
await index.write({ path: 'indexes/articles.qvi' })
await index.dispose()

const reopened = await loadVectorIndex({ path: 'indexes/articles.qvi' })

- add a `vectorIndex` wire method with create/load/add/search/remove/contains/write/dispose operations, served by a worker-side index registry that obtains the native index through the existing turbovecIndexProvider plugin capability
- export `createVectorIndex` and `loadVectorIndex` from @qvac/sdk and @qvac/inference; both return a handle object with add/search/remove/contains/write/dispose and Symbol.asyncDispose
- ids travel as decimal uint64 strings or safe integers, vectors as number rows, so generated clients need no encoding helpers; register the storage vocabulary as a contract constant
- add VECTOR_INDEX_* error codes, dispose open indexes on worker cleanup, resolve relative snapshot paths under the QVAC data directory
- regenerate the SDK contract and the Python client; add inference, SDK unit, and e2e coverage; add the rag-turbovec example and a RAG guide section
@lauripiisang
lauripiisang requested review from a team as code owners September 14, 2026 15:47
@github-actions

Copy link
Copy Markdown
Contributor

Review Status

Current Status: ❌ PENDING
Approvals so far: none

Pending reviews: Needs 1 Management or Team Lead, and 1 more from Management, Team Lead, or Member.

@github-actions

Copy link
Copy Markdown
Contributor

License compliance — clean

No new dependency license findings in this PR.

Warn-only (shadow) mode — this check does not block merges yet.

Updated automatically by the canonical license compliance workflow.

NOTICE presence (advisory)

Missing NOTICE (advisory, does not block):

  • ./docs/website
  • ./packages/fabric/test/integration
  • ./packages/llm-llamacpp/benchmarks/server
  • ./packages/llm-llamacpp/benchmarks/performance
  • ./packages/inference-addon-cpp/mobile
  • ./packages/asr-ggml/benchmarks/server
  • ./packages/embed-llamacpp/benchmarks/server
  • ./packages/embed-llamacpp/benchmarks/performance
  • ./packages/sdk/e2e
  • ./packages/vla-ggml/sim/server
  • ./.github/actions/release-merge-guard

- move the handler to handlers/vector-index.ts and the worker registry to runtime/vector-index-registry.ts, matching the layout of the other reply features
- add VectorIndexProvider / VectorIndexBackend aliases and getVectorIndexProvider() so the one place binding the feature to the TurboVec capability is named; the registry no longer imports TurboVec types
- declare VectorIndexStorage as a name-to-value constant like ModelType and use it in the example, docs, and contract registry
…d of asserting it

- search the reloaded index and fail the run when its best match differs from the original
- add an inference-owned VectorIndexBackend / OpenVectorIndex interface with finished hit rows and no engine-specific calls
- add plugins/turbovec-backend.ts, the one adapter over the turbovecIndexProvider capability; it owns the prepare warm-up, the UINT64_MAX padding sentinel, and storage-mode reporting
- reduce runtime/vector-index-registry.ts to bookkeeping: ids, row validation, snapshot paths, lifetime, error wrapping
- drop the VectorIndexProvider / VectorIndexBackend aliases and getVectorIndexProvider() that the adapter replaces
- make the provider fixture pad result rows like the native index and test that padding is stripped
- derive the storage mode of a loaded index from the native bit width; the addon exposes bitWidth, never a storage property, so the previous read was dead code and every loaded index reported undefined. 4-bit snapshots stay undefined because q4 and turbovec-q4 share a width
- set the handle's disposed flag only after the worker confirms, so a rejected dispose can be retried instead of leaking the index until worker shutdown
- wrap the snapshot mkdir in VECTOR_INDEX_FAILED, as the write call beside it already was
- give the e2e snapshot a fixed name so repeated runs overwrite one file, and assert the reloaded storage mode
- drop readVectorIndexStorage, which had no remaining caller
@lauripiisang lauripiisang added the test-e2e-smoke Triggers smoke e2e test suite [Currently SDK-only] label Sep 14, 2026
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — base run recorded

Base: run 34877799360 · suite smoke · sha c4e7b04

Platform Passed Total Failed
android/android 93 108 0
desktop/linux 108 108 0
desktop/macos 108 108 0
desktop/windows 108 108 0
ios/ios 93 108 0

All recorded platforms are green — nothing for test-e2e-rerun-failed to re-run.

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — ios — ✅ all tests passed (93/108, 758s)

Config: suite=smoke · filter=(none) · exclude=(none)
Inference: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/inference@0.19.1 · Test-suite: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/test-suite@0.11.2
Device pool: iPhone 17 Pro - iOS 26 — ❔ unknown
View run · Artifacts: reports · Device Farm logs

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — windows — ✅ all tests passed (108/108, 692s)

Config: suite=smoke · filter=(none) · exclude=(none)
Inference: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/inference@0.19.1 · Test-suite: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/test-suite@0.11.2
View run · Artifacts: reports

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — linux — ✅ all tests passed (108/108, 569s)

Config: suite=smoke · filter=(none) · exclude=(none)
Inference: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/inference@0.19.1 · Test-suite: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/test-suite@0.11.2
View run · Artifacts: reports

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — macos — ✅ all tests passed (108/108, 440s)

Config: suite=smoke · filter=(none) · exclude=(none)
Inference: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/inference@0.19.1 · Test-suite: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/test-suite@0.11.2
View run · Artifacts: reports

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

QVAC E2E — android — ✅ all tests passed (93/108, 1650s)

Config: suite=smoke · filter=(none) · exclude=(none)
Inference: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/inference@0.19.1 · Test-suite: branch:c4e7b04a16dda6007897eaeac1e21661b02f97f7:@qvac/test-suite@0.11.2
Device pool: Samsung S26 Ultra - Android 16 — ❔ unknown
View run · Artifacts: reports · Device Farm logs

Both conflicts were in generated artifacts (contract/schema.json and the
Python _generated models) where main's TTS and diffusion work met the new
vectorIndex method. Resolved by regenerating both from the merged sources
rather than hand-merging.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-e2e-smoke Triggers smoke e2e test suite [Currently SDK-only]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants