Files
LocalAI/docs/content/features/stores.md
localai-org-maint-bot 9bfd71387b feat(stores): add Valkey Search vector store backend (#11196)
* 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>
2026-07-29 20:12:29 +02:00

9.0 KiB

+++ disableToc = false title = "Stores" weight = 62 url = '/stores' +++

Stores are an experimental feature to help with querying data using similarity search. It is a low level API that consists of only get, set, delete and find.

{{% notice tip %}} Face recognition uses this store. The 1:N face identification flow (/v1/face/register, /v1/face/identify, /v1/face/forget) is built on top of the generic store - see Face Recognition for the face-oriented API. {{% /notice %}}

For example if you have an embedding of some text and want to find text with similar embeddings. You can create embeddings for chunks of all your text then compare them against the embedding of the text you are searching on.

An embedding here meaning a vector of numbers that represent some information about the text. The embeddings are created from an A.I. model such as BERT or a more traditional method such as word frequency.

Previously you would have to integrate with an external vector database or library directly. With the stores feature you can now do it through the LocalAI API.

Note however that doing a similarity search on embeddings is just one way to do retrieval. A higher level API can take this into account, so this may not be the best place to start.

API overview

There is an internal gRPC API and an external facing HTTP JSON API. We'll just discuss the external HTTP API, however the HTTP API mirrors the gRPC API. Consult pkg/store/client for internal usage.

Everything is in columnar format meaning that instead of getting an array of objects with a key and a value each. You instead get two separate arrays of keys and values.

Keys are arrays of floating point numbers with a maximum width of 32bits. Values are strings (in gRPC they are bytes).

The key vectors must all be the same length and it's best for search performance if they are normalized. When addings keys it will be detected if they are not normalized and what length they are.

All endpoints accept a store field which specifies which store to operate on. Stores are created on the fly. By default the in-memory local-store backend is used and no configuration is required, but you can select a different store backend per request (see Backends below).

Backends

Each /stores/* request accepts an optional backend field selecting the store implementation. Two backends ship with LocalAI:

Backend backend value Persistence Notes
Local (default) local-store (alias embedded-store) In-memory, lost on restart Exact cosine similarity, zero configuration.
Valkey Search valkey-store (alias valkey) Durable (Valkey RDB/AOF) Backed by a Valkey Search (FT.*) server; survives restarts and supports opt-in HNSW.

Valkey store backend

The valkey-store backend persists vectors in a Valkey Search server, so the data survives a LocalAI restart — unlike the in-memory default. It requires a reachable server that ships the Valkey Search module (for example the valkey/valkey-bundle image).

Select it by passing "backend": "valkey-store" (or the "valkey" alias) on any /stores/* request:

curl -X POST http://localhost:8080/stores/set \
     -H "Content-Type: application/json" \
     -d '{"backend": "valkey-store", "store": "my-vectors", "keys": [[0.1, 0.2], [0.3, 0.4]], "values": ["foo", "bar"]}'

The connection and index are configured through a model config named after the store (the store field on the request, which is the store's model ID). Create a YAML in your models directory whose name matches the store, set backend: valkey-store, and put the connection / index settings in the options: list as key:value strings. Because each store resolves its own config, different stores can point at different Valkey servers or use different index settings within one LocalAI process:

name: my-vectors
backend: valkey-store
options:
  - addr:valkey.internal:6379
  - username_env:MY_VALKEY_USER
  - password_env:MY_VALKEY_PASSWORD
  - index_algo:HNSW
  - distance_metric:COSINE

The username_env / password_env options name an environment variable that holds the actual credential rather than putting the secret directly in the YAML. This mirrors cloud-proxy's api_key_env pattern and lets distinct store configs each reference their own credentials. The direct username / password options still work for backward compatibility, and take precedence when both are set.

When no config exists for a store, the backend connects to localhost:6379 with the defaults below (so the zero-config experience still works).

Option Default Description
addr localhost:6379 Valkey server address (host:port).
username (empty) Optional ACL username (plaintext in config).
password (empty) Optional password / ACL secret (plaintext in config).
username_env (empty) Name of an env var that holds the username. Preferred over username for secrets — keeps credentials out of the model YAML.
password_env (empty) Name of an env var that holds the password. Preferred over password for secrets.
tls false Enable TLS (required by many managed deployments).
tls_ca_cert (empty) Path to a PEM CA bundle used to verify the server certificate (self-signed / private CA).
tls_skip_verify false Skip TLS certificate verification. Insecure — for testing only.
client_name localai-valkey-store Connection name reported by CLIENT LIST. Always set.
db 0 Logical Valkey DB index (SELECT n). Namespace prefixing already isolates keyspaces on a shared DB.
index_algo FLAT FLAT (exact, default) or HNSW (approximate ANN for large corpora).
hnsw_m 16 HNSW graph degree (only when index_algo:HNSW).
hnsw_ef_construction 200 HNSW build-time candidate list (HNSW only).
hnsw_ef_runtime 10 HNSW query-time candidate list (HNSW only).
distance_metric COSINE COSINE (default), L2 or IP.
request_timeout_ms 5000 Per-command timeout in milliseconds.

For COSINE the returned similarities follow the same convention as the local store (1.0 = identical, -1.0 = opposite); internally Valkey returns a cosine distance which the backend converts with similarity = 1 - distance. For L2 and IP the raw Valkey score is returned in similarities (for L2, smaller means closer — the opposite ordering of COSINE); nearest-first ordering of the results is preserved in all cases.

{{% notice note %}} Valkey Search updates its vector index asynchronously after a write. A find issued immediately after set may not yet see the new vectors — poll find briefly (or retry) until the expected results appear. get and delete are synchronous and unaffected. {{% /notice %}}

{{% notice note %}} This backend targets a standalone Valkey Search server (one server per namespace/model). Valkey Cluster is not a supported target yet — index coordination across shards is out of scope for this backend. {{% /notice %}}

{{% notice warning %}} tls defaults to false (plaintext). Set tls:true whenever the Valkey server is not on localhost or a password/username is configured, otherwise the credentials and the stored vectors travel the network unencrypted. The TLS ServerName (SNI) is derived from the host portion of addr, so certificate verification works for both hostname and IP-addressed endpoints. For a self-signed / private CA, point tls_ca_cert at the PEM bundle; tls_skip_verify:true disables verification entirely and should only be used for local testing. {{% /notice %}}

Set

To set some keys you can do

curl -X POST http://localhost:8080/stores/set \
     -H "Content-Type: application/json" \
     -d '{"keys": [[0.1, 0.2], [0.3, 0.4]], "values": ["foo", "bar"]}'

Setting the same keys again will update their values.

On success 200 OK is returned with no body.

Get

To get some keys you can do

curl -X POST http://localhost:8080/stores/get \
     -H "Content-Type: application/json" \
     -d '{"keys": [[0.1, 0.2]]}'

Both the keys and values are returned, e.g: {"keys":[[0.1,0.2]],"values":["foo"]}

The order of the keys is not preserved! If a key does not exist then nothing is returned.

Delete

To delete keys and values you can do

curl -X POST http://localhost:8080/stores/delete \
     -H "Content-Type: application/json" \
     -d '{"keys": [[0.1, 0.2]]}'

If a key doesn't exist then it is ignored.

On success 200 OK is returned with no body.

Find

To do a similarity search you can do

curl -X POST http://localhost:8080/stores/find 
     -H "Content-Type: application/json" \
     -d '{"topk": 2, "key": [0.2, 0.1]}'

topk limits the number of results returned. The result value is the same as get, except that it also includes an array of similarities. Where 1.0 is the maximum similarity. They are returned in the order of most similar to least.