mirror of
https://github.com/mudler/LocalAI.git
synced 2026-08-01 19:09:42 -04:00
* feat: add Valkey Search vector store backend 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> * Address review feedback: recover persisted index dimension, harden Find - 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> * Address review feedback: TLS ServerName/CA, Find nil-check, config fail-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> * Address review feedback: configure valkey-store via model config 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> * Remove valkey-store informational comment from .env The backend is configured via model config, not env vars — the comment was unnecessary noise in .env. The configuration is already documented in docs/content/features/stores.md. Signed-off-by: Daria Korenieva <daric2612@gmail.com> * feat(valkey-store): gate Load on NamespacePrefix to refuse autoload probing Mirror local-store's pattern: reject model names without store.NamespacePrefix so the model loader's greedy autoload probe cannot bind an arbitrary model name to the vector store backend (the #9287 failure mode). Also adds unit tests for the gate covering: prefixed namespace, prefix alone, unprefixed model name, empty model, and nil opts. Signed-off-by: Daria Korenieva <daric2612@gmail.com> * feat(valkey-store): add username_env/password_env credential indirection Add support for resolving Valkey credentials from environment variables named in the model config, mirroring cloud-proxy's api_key_env pattern. This keeps secrets out of model YAML files and lets distinct store configs each reference their own credentials. Options: username_env / password_env name the env var holding the value. The direct username / password options still work and take precedence when both are set (backward compatible). Includes 5 unit tests and updated stores.md documentation. Signed-off-by: Daria Korenieva <daric2612@gmail.com> * fix: correct rebase artifacts in backend-matrix.yml and Makefile Fix two issues introduced by the conflict-resolution script during the rebase onto master: 1. .github/backend-matrix.yml: valkey-store entries were merged INTO the cloud-proxy entries (duplicate keys in same YAML map items) instead of being separate list items. This broke cloud-proxy Linux builds and the cloud-proxy darwin entry lost its build-type/lang. Fixed by making them standalone entries and restoring cloud-proxy exactly as on master. 2. Makefile: duplicated .NOTPARALLEL and docker-build-backends lines. Collapsed to single lines that are master's current content plus the valkey-store additions. Also adds the three optional pickups from #10801: - /valkey-store in .gitignore (the built binary) - valkey-store row in docs/content/reference/compatibility-table.md - valkey-store line in backend/README.md Signed-off-by: Daria Korenieva <daric2612@gmail.com> --------- Signed-off-by: Daria Korenieva <daric2612@gmail.com> Co-authored-by: Daria Korenieva <daric2612@gmail.com>
78 lines
2.4 KiB
Go
78 lines
2.4 KiB
Go
package main
|
|
|
|
// Unit tests for the vector⇄key encoding. These need no Valkey server: they
|
|
// exercise the pure lossless-encoding contract that the whole store relies on,
|
|
// including the documented edge cases (-0.0/+0.0 and NaN) where this encoding
|
|
// intentionally diverges from local-store's slices.Compare float equality.
|
|
|
|
import (
|
|
"math"
|
|
"math/rand/v2"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
valkey "github.com/valkey-io/valkey-go"
|
|
)
|
|
|
|
var _ = Describe("vector⇄bytes encoding", func() {
|
|
It("round-trips vectors of varying dimensions", func() {
|
|
r := rand.New(rand.NewPCG(1, 2))
|
|
for _, dim := range []int{1, 3, 4, 16, 128, 768} {
|
|
v := make([]float32, dim)
|
|
for i := range v {
|
|
v[i] = float32(r.NormFloat64())
|
|
}
|
|
got, err := bytesToVec(vecToBytes(v))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(got).To(Equal(v))
|
|
}
|
|
})
|
|
|
|
It("matches valkey.VectorString32 byte-for-byte", func() {
|
|
// The stored `vec` field uses valkey.VectorString32; the key uses
|
|
// vecToBytes. They must be the same encoding or Get/Find break.
|
|
v := []float32{0.1, -0.2, 3.5, 0}
|
|
Expect(valkey.BinaryString(vecToBytes(v))).To(Equal(valkey.VectorString32(v)))
|
|
})
|
|
|
|
It("rejects a byte payload that is not a multiple of 4", func() {
|
|
_, err := bytesToVec([]byte{1, 2, 3})
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
var _ = Describe("key encoding", func() {
|
|
const prefix = "vs:test:"
|
|
|
|
It("round-trips key encode/decode", func() {
|
|
v := []float32{0.5, 0.5, 0.5}
|
|
key := encodeKey(prefix, v)
|
|
Expect(key).To(HavePrefix(prefix))
|
|
got, err := decodeKey(prefix, key)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(got).To(Equal(v))
|
|
})
|
|
|
|
It("produces distinct keys for -0.0 and +0.0 (documented divergence)", func() {
|
|
negZero := float32(math.Copysign(0, -1))
|
|
posZero := float32(0)
|
|
Expect(math.Signbit(float64(negZero))).To(BeTrue())
|
|
Expect(encodeKey(prefix, []float32{negZero})).NotTo(Equal(encodeKey(prefix, []float32{posZero})))
|
|
})
|
|
|
|
It("produces a stable, distinct key for a NaN component", func() {
|
|
nan := float32(math.NaN())
|
|
k1 := encodeKey(prefix, []float32{nan})
|
|
k2 := encodeKey(prefix, []float32{nan})
|
|
// Deterministic: same NaN bit-pattern → same key.
|
|
Expect(k1).To(Equal(k2))
|
|
// Distinct from a normal value.
|
|
Expect(k1).NotTo(Equal(encodeKey(prefix, []float32{0})))
|
|
})
|
|
|
|
It("rejects a key without the expected prefix", func() {
|
|
_, err := decodeKey(prefix, "wrong:deadbeef")
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
})
|