Skip to content

feat(stores): add Valkey Search vector store backend#10770

Open
daric93 wants to merge 3 commits into
mudler:masterfrom
daric93:feature/valkey-store-backend
Open

feat(stores): add Valkey Search vector store backend#10770
daric93 wants to merge 3 commits into
mudler:masterfrom
daric93:feature/valkey-store-backend

Conversation

@daric93

@daric93 daric93 commented Jul 10, 2026

Copy link
Copy Markdown

Description

Adds 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.* vector similarity), selectable via the existing per-request backend field on the /stores endpoints. It mirrors the in-memory local-store and adds persistence across restarts plus opt-in HNSW.

  • No proto change, no HTTP/API change — selected with "backend": "valkey-store" (alias "valkey").
  • Client: github.com/valkey-io/valkey-go v1.0.76 (official, pure Go, no CGO).
  • Each vector is a Valkey HASH keyed by hex(little-endian float32); the FT index is created lazily on first Set (FLAT + COSINE by default), and cosine similarity is derived as 1 - distance to match local-store semantics exactly.
  • Per-namespace, collision-resistant key prefix / index name (sanitize(name) + short sha256).
  • Config via VALKEY_* env vars (addr, auth, TLS, index algo + HNSW knobs, distance metric, per-command request timeout, mandatory ClientName).

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 its Find is 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-go and not valkey-glide? The official valkey-glide Go client is built on a Rust core via FFI, which forces CGO and a per-arch native library — that breaks local-store's CGO_ENABLED=0 static build and the Linux/Darwin backend matrix. valkey-go is pure Go with a typed FT.SEARCH builder, a VectorString32 encoder, and a gomock mock package for unit tests.

Testing

  • Unit tests (valkey-go mock, no container): 35 specs — RPC command-shape/wire contract, empty/len/dim rejects, omit-missing Get, tolerate-missing Delete, topK<1 reject, distance→similarity conversion (0→1, 1→0, 2→−1), lazy FT.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.
  • Integration tests (Ginkgo, Label("valkey"), env-gated on VALKEY_ADDR, against valkey/valkey-bundle:9.1.0 on :6379): 14 specs mirroring the local-store suite 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 a CLIENT LIST client-name assertion. Index back-fill is absorbed with a bounded find poll (no time.Sleep). Skipped automatically when VALKEY_ADDR is unset, so unit CI needs no container.
  • golangci-lint clean on the changed packages. All Valkey logic lives in backend/go/valkey-store/ (outside COVERAGE_COVERPKG), so the coverage ratchet is unaffected.

Run integration locally:

podman run -d --name valkey-store-it -p 6379:6379 valkey/valkey-bundle:9.1.0
make backends/valkey-store
VALKEY_ADDR=localhost:6379 make test-valkey-store

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

  • Yes, I signed my commits.

@daric93 daric93 force-pushed the feature/valkey-store-backend branch from f72d11f to 17c3e0a Compare July 10, 2026 20:26
@daric93 daric93 marked this pull request as ready for review July 10, 2026 20:28
daric93 added 3 commits July 10, 2026 13:31
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>
@daric93 daric93 force-pushed the feature/valkey-store-backend branch from 17c3e0a to 86ac171 Compare July 10, 2026 20:33
@mudler mudler requested a review from richiejp July 11, 2026 07:19
Comment thread .env
# VALKEY_HNSW_M=16
# VALKEY_HNSW_EF_CONSTRUCTION=200
# VALKEY_HNSW_EF_RUNTIME=10
# VALKEY_REQUEST_TIMEOUT_MS=5000

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at least some of these should go in a model config. Then you can have multiple stores configs.

}

BeforeEach(func() {
valkeyAddr = os.Getenv("VALKEY_ADDR")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid putting env accesses throughout the code. Model config is the natural place instead of env vars.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants