feat(stores): add Valkey Search vector store backend#10770
Conversation
f72d11f to
17c3e0a
Compare
Add a new built-in Go gRPC store backend 'valkey-store' that implements the four Stores RPCs (Set/Get/Delete/Find) against the Valkey Search module (FT.*) using the pure-Go github.com/valkey-io/valkey-go client. It is selected via the existing per-request 'backend' field on /stores, so there is no proto or HTTP API change, and it mirrors the in-memory local-store while adding persistence across restarts and opt-in HNSW. Each vector is a Valkey HASH keyed by hex(little-endian float32); the index is created lazily on first Set (FLAT+COSINE by default), cosine similarity is derived as 1-distance, and namespaces get a collision-resistant token. Includes unit tests (valkey-go mock) and env-gated integration tests against valkey/valkey-bundle, plus build/matrix/gallery wiring and docs. Assisted-by: Kiro:claude-opus-4.8 golangci-lint Signed-off-by: Daria Korenieva <daric2612@gmail.com>
- Load now recovers the persisted vector DIM from FT.INFO (not just index existence), so a post-restart Set/Find validates against the real DIM instead of silently re-learning a wrong one and dropping mismatched vectors from the index. This also restores Find's dimension check after a restart. - StoresFind treats a dropped/missing index as an empty store (empty result, no error) and clears the stale indexCreated flag, matching local-store's empty-store behaviour. - StoresSet reuses checkDims for its per-key length check so the four RPCs share one dimension-guard implementation. - Add unit tests for FT.INFO dimension recovery, loadIndexState, and the dropped-index Find path. Assisted-by: Kiro:claude-opus-4.8 Signed-off-by: Daria Korenieva <daric2612@gmail.com>
…il-fast Addresses external review comments on the valkey-store backend: - StoresFind now rejects a nil/empty query Key before dereferencing it, so a malformed gRPC request can no longer panic the backend. - TLS: derive ServerName (SNI) from the VALKEY_ADDR host so certificate verification works for IP-addressed endpoints, and add VALKEY_TLS_CA_CERT (custom CA bundle) and VALKEY_TLS_SKIP_VERIFY (testing-only) knobs. - Config integer parsing now fails fast on a malformed value (e.g. VALKEY_HNSW_M=1x6) instead of silently defaulting, matching the fail-fast behaviour of the index-algo/distance-metric validation. - Add VALKEY_DB (SELECT n) support for logical-DB isolation. - Cap the human-readable part of a namespace token at 64 chars so a very long model name cannot produce an unbounded key prefix / index name (the appended short hash keeps distinct namespaces collision-free). - Document the KNN-query injection-safety invariant (fields are constants) and why StoresGet uses a single aggregate DoMulti deadline for reads. - Unit tests for the Find nil/empty-key guard, fail-fast HNSW parsing, and VALKEY_DB parsing/validation; docs + .env updated for the new vars. Assisted-by: Kiro:claude-opus-4.8 golangci-lint Signed-off-by: Daria Korenieva <daric2612@gmail.com>
17c3e0a to
86ac171
Compare
| # VALKEY_HNSW_M=16 | ||
| # VALKEY_HNSW_EF_CONSTRUCTION=200 | ||
| # VALKEY_HNSW_EF_RUNTIME=10 | ||
| # VALKEY_REQUEST_TIMEOUT_MS=5000 |
There was a problem hiding this comment.
I think at least some of these should go in a model config. Then you can have multiple stores configs.
There was a problem hiding this comment.
Done — the backend now reads its config from the store's model config instead of VALKEY_* env vars. A store is configured with a model YAML (name: = the store, backend: valkey-store, and an options: list like addr:host:6379 / index_algo:HNSW), so different stores can each point at their own Valkey server/index within one process. Dropped the env var block from .env and documented the options in stores.md.
| } | ||
|
|
||
| BeforeEach(func() { | ||
| valkeyAddr = os.Getenv("VALKEY_ADDR") |
There was a problem hiding this comment.
Please avoid putting env accesses throughout the code. Model config is the natural place instead of env vars.
There was a problem hiding this comment.
Done — removed all env access from the backend. loadConfig now parses the model-config options: list threaded through LoadModel (ModelOptions.Options), and core/backend/stores.go resolves each store's ModelConfig and passes its options down. The integration test configures the store through those same options; VALKEY_ADDR remains only as the harness gate that locates the test server, not as backend config.
…ore-backend Signed-off-by: Daria Korenieva <daric2612@gmail.com> # Conflicts: # Makefile
richiejp asked that the valkey-store backend take its configuration from a model config rather than process-wide VALKEY_* environment variables, so multiple stores can each have their own Valkey config within one LocalAI process. This removes every env access from the backend and routes config through the model-config seam every other backend uses. - config.go: loadConfig(opts *pb.ModelOptions) now parses the model config `options:` list (key:value strings, split on the first ':') instead of os.Getenv. Option keys mirror the old VALKEY_* names without the prefix (addr, index_algo, distance_metric, ...). Defaults, fail-fast validation and the mandatory client name are unchanged. - store.go: Load threads opts into loadConfig; TLS comments/errors renamed off the VALKEY_* names. - core/backend/stores.go: StoreBackend and NewVectorStore take a *config.ModelConfigLoader, resolve the per-store ModelConfig by store name, and pass its Options (and Backend when unset) to the backend via WithLoadGRPCLoadModelOpts. No config -> default backend + built-in defaults, preserving the zero-config experience. - Endpoints/routes/application: thread the config loader to StoreBackend. - Unit + integration tests: configure via options; the integration test passes addr through the model-config path (VALKEY_ADDR is now only the test harness locating the server). - docs + .env: document the model-config options, drop the env var table. Assisted-by: Kiro:claude-opus-4.8 Signed-off-by: Daria Korenieva <daric2612@gmail.com>
Description
Closes: #10708
Adds a new built-in Go gRPC store backend
valkey-storethat implements the fourStores*RPCs (Set / Get / Delete / Find) against the Valkey Search module (FT.*vector similarity), selectable via the existing per-requestbackendfield on the/storesendpoints. It mirrors the in-memorylocal-storeand adds persistence across restarts plus opt-in HNSW."backend": "valkey-store"(alias"valkey").github.com/valkey-io/valkey-gov1.0.76 (official, pure Go, no CGO).hex(little-endian float32); theFTindex is created lazily on firstSet(FLAT + COSINE by default), and cosine similarity is derived as1 - distanceto matchlocal-storesemantics exactly.sanitize(name) + short sha256).VALKEY_*env vars (addr, auth, TLS, index algo + HNSW knobs, distance metric, per-command request timeout, mandatoryClientName).Why
The stores subsystem is designed to be pluggable, but the only shipped backend (
local-store) is in-memory and loses all data on restart, and itsFindis an O(N) scan. Face/voice biometric registries and the router embedding cache all consume this seam, so a durable, scalable Valkey-backed backend benefits them with zero caller changes.Why
valkey-goand notvalkey-glide? The officialvalkey-glideGo client is built on a Rust core via FFI, which forces CGO and a per-arch native library — that breakslocal-store'sCGO_ENABLED=0static build and the Linux/Darwin backend matrix.valkey-gois pure Go with a typedFT.SEARCHbuilder, aVectorString32encoder, and a gomock mock package for unit tests.Testing
valkey-gomock, no container): 35 specs — RPC command-shape/wire contract, empty/len/dim rejects, omit-missing Get, tolerate-missing Delete,topK<1reject, distance→similarity conversion (0→1, 1→0, 2→−1), lazyFT.CREATE, HNSW arg-shape, key-encoding round-trip (incl.-0.0/NaN), namespace-token collision resistance, config parsing.make test/go run ...ginkgo -r backend/go/valkey-store.Label("valkey"), env-gated onVALKEY_ADDR, againstvalkey/valkey-bundle:9.1.0on :6379): 14 specs mirroring thelocal-storesuite one-for-one (set/get/delete/find, exact cosine for orthogonal/opposite unit + non-unit vectors, triangle inequality incl. random 768-d) plus a persistence/restart test and aCLIENT LISTclient-name assertion. Index back-fill is absorbed with a boundedfindpoll (notime.Sleep). Skipped automatically whenVALKEY_ADDRis unset, so unit CI needs no container.golangci-lintclean on the changed packages. All Valkey logic lives inbackend/go/valkey-store/(outsideCOVERAGE_COVERPKG), so the coverage ratchet is unaffected.Run integration locally:
Scope / follow-ups
Purely additive and opt-in per request. Deferred to follow-ups: hybrid / metadata-filter search (needs new API surface beyond the four RPCs), Valkey Cluster mode, and single-instance multi-namespace consolidation.
Notes for Reviewers
cc @mudler
Signed commits