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>
81 lines
3.1 KiB
Go
81 lines
3.1 KiB
Go
package main
|
|
|
|
// Vector⇄key encoding: the "vector IS the key" resolution.
|
|
//
|
|
// local-store keys entries *by* the vector itself (a []float32). Valkey hashes
|
|
// are keyed by strings, so we synthesise a deterministic, lossless key:
|
|
//
|
|
// key = prefix + hex(little-endian float32 bytes of the vector)
|
|
//
|
|
// The same vector always produces the same bytes, so HSET is an upsert and
|
|
// HGET/DEL are exact matches — and the encoding is reversible, so we can hand
|
|
// the original []float32 back on Get/Find.
|
|
//
|
|
// Divergence from local-store (documented and tested): local-store compares
|
|
// keys with slices.Compare, which treats -0.0 == +0.0 and orders NaN, so those
|
|
// collapse to the same logical key. Byte-encoding makes -0.0 and +0.0 (and any
|
|
// distinct NaN bit-pattern) *distinct* keys. We accept this on purpose: a
|
|
// lossless, deterministic, exact round-trip is more valuable for a persistent
|
|
// store than reproducing local-store's float-equality quirk, and callers never
|
|
// rely on -0.0/+0.0 aliasing.
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
// _float32Bytes is the wire width of a single FLOAT32 component.
|
|
const _float32Bytes = 4
|
|
|
|
// vecToBytes encodes a vector as little-endian float32 bytes. This is byte-for
|
|
// -byte identical to valkey.VectorString32, so the value we store in the hash
|
|
// `vec` field and the bytes we hash into the key share one encoding.
|
|
func vecToBytes(v []float32) []byte {
|
|
b := make([]byte, len(v)*_float32Bytes)
|
|
for i, e := range v {
|
|
off := i * _float32Bytes
|
|
binary.LittleEndian.PutUint32(b[off:off+_float32Bytes], math.Float32bits(e))
|
|
}
|
|
return b
|
|
}
|
|
|
|
// bytesToVec reverses vecToBytes. It rejects a payload whose length is not a
|
|
// multiple of the float32 width, which would indicate a corrupted/foreign value.
|
|
func bytesToVec(b []byte) ([]float32, error) {
|
|
if len(b)%_float32Bytes != 0 {
|
|
return nil, fmt.Errorf("valkey-store: vector byte length %d is not a multiple of %d", len(b), _float32Bytes)
|
|
}
|
|
v := make([]float32, len(b)/_float32Bytes)
|
|
for i := range v {
|
|
off := i * _float32Bytes
|
|
v[i] = math.Float32frombits(binary.LittleEndian.Uint32(b[off : off+_float32Bytes]))
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// encodeKey builds the Valkey hash key for a vector: prefix + hex(bytes).
|
|
// Hex keeps the key printable (so it is safe in FT.CREATE PREFIX and in logs)
|
|
// while staying lossless.
|
|
func encodeKey(prefix string, v []float32) string {
|
|
return prefix + hex.EncodeToString(vecToBytes(v))
|
|
}
|
|
|
|
// decodeKey reverses encodeKey. It is intentionally retained as the tested,
|
|
// symmetric inverse of encodeKey — it is NOT on the hot Find path (StoresFind
|
|
// decodes the returned `vec` bytes via bytesToVec directly), but keeping the
|
|
// key↔vector mapping provably invertible guards the encoding contract and is
|
|
// exercised by the round-trip unit tests.
|
|
func decodeKey(prefix, key string) ([]float32, error) {
|
|
if !strings.HasPrefix(key, prefix) {
|
|
return nil, fmt.Errorf("valkey-store: key %q does not have expected prefix %q", key, prefix)
|
|
}
|
|
b, err := hex.DecodeString(strings.TrimPrefix(key, prefix))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("valkey-store: decode key hex: %w", err)
|
|
}
|
|
return bytesToVec(b)
|
|
}
|