Skip to content

Commit d413ef9

Browse files
committed
feat(valkey-store): make connection settings configurable per model
Address review feedback on PR #10801: VALKEY_ADDR, VALKEY_USERNAME, VALKEY_PASSWORD, and VALKEY_INDEX_ALGO were process-wide env vars only, so every model sharing this backend had to use the same Valkey server and credentials. Add matching model-config options (valkey_addr, valkey_index_algo, valkey_username_env, valkey_password_env) that take priority over the env vars when set. The credential options name an env var to read from rather than taking the value directly, mirroring cloud-proxy's api_key_env/api_key_file pattern, so multiple valkey-store model configs can each point at a different server/credential pair without putting secrets in the YAML. Existing env-var-only configs keep working unchanged (the options are purely additive fallthrough). Added unit tests for the new resolution helpers and a live-server test proving the model option wins over the env var end-to-end; all 27 specs pass (golangci-lint, go vet, gofmt clean). Assisted-by: Claude Code:claude-sonnet-5 golangci-lint go test
1 parent d300ef1 commit d413ef9

3 files changed

Lines changed: 125 additions & 15 deletions

File tree

backend/go/valkey-store/store.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,15 @@ package main
2222
// Search parity: the index defaults to FLAT with COSINE distance, so
2323
// Find is an exact scan whose ranking matches local-store; Valkey
2424
// returns cosine *distance* and similarity is derived as
25-
// `1 - distance`. Set VALKEY_INDEX_ALGO=HNSW for approximate ANN on
26-
// large corpora.
25+
// `1 - distance`. Set VALKEY_INDEX_ALGO=HNSW (or the valkey_index_algo
26+
// model option) for approximate ANN on large corpora.
27+
//
28+
// Connection settings resolve per-model first, then fall back to the
29+
// process-wide env vars: `valkey_addr` / VALKEY_ADDR, `valkey_index_algo`
30+
// / VALKEY_INDEX_ALGO, and — mirroring cloud-proxy's api_key_env —
31+
// `valkey_username_env` / `valkey_password_env` name the env vars that
32+
// actually hold the credential, so distinct model configs can each
33+
// point at a different Valkey server with its own credentials.
2734
//
2835
// Concurrency: base.SingleThread serialises gRPC calls, matching
2936
// local-store; Valkey itself is the durable shared state.
@@ -71,12 +78,43 @@ type Store struct {
7178
// index, so dimension mismatches fail loudly instead of
7279
// producing bogus cosine matches.
7380
keyLen int
81+
82+
// indexAlgo is resolved once at Load (model option, else env var,
83+
// else FLAT) and reused by createIndex, which runs later from
84+
// StoresSet and has no access to the ModelOptions that carried it.
85+
indexAlgo string
7486
}
7587

7688
func NewStore() *Store {
7789
return &Store{keyLen: -1}
7890
}
7991

92+
// optString reads a model option (key:value form) from ModelOptions,
93+
// returning def when absent. Mirrors optInt in sibling backends
94+
// (e.g. moss-transcribe-cpp) for the model YAML's `options:` list.
95+
func optString(opts *pb.ModelOptions, key, def string) string {
96+
for _, o := range opts.GetOptions() {
97+
k, v, ok := strings.Cut(o, ":")
98+
if ok && strings.TrimSpace(k) == key {
99+
return strings.TrimSpace(v)
100+
}
101+
}
102+
return def
103+
}
104+
105+
// optEnvIndirect resolves a credential the way cloud-proxy's
106+
// api_key_env does: the model option names an env var to read from,
107+
// so distinct model configs can each reference a different credential
108+
// pair without putting secrets in the YAML. Falls back to a single
109+
// process-wide env var when the model doesn't set the option, keeping
110+
// the single-Valkey-server default working unchanged.
111+
func optEnvIndirect(opts *pb.ModelOptions, optKey, fallbackEnvVar string) string {
112+
if envVar := optString(opts, optKey, ""); envVar != "" {
113+
return os.Getenv(envVar)
114+
}
115+
return os.Getenv(fallbackEnvVar)
116+
}
117+
80118
// Load validates the namespace, connects to Valkey and, when the
81119
// namespace was already populated by a previous process, restores the
82120
// index dimension from the meta hash. The namespace-prefix gate keeps
@@ -93,14 +131,15 @@ func (s *Store) Load(opts *pb.ModelOptions) error {
93131
s.prefix = keyspacePrefix + ns + ":"
94132
s.index = s.prefix + "idx"
95133

96-
addr := os.Getenv("VALKEY_ADDR")
134+
addr := optString(opts, "valkey_addr", os.Getenv("VALKEY_ADDR"))
97135
if addr == "" {
98136
addr = "127.0.0.1:6379"
99137
}
138+
s.indexAlgo = optString(opts, "valkey_index_algo", os.Getenv("VALKEY_INDEX_ALGO"))
100139
client, err := valkey.NewClient(valkey.ClientOption{
101140
InitAddress: []string{addr},
102-
Username: os.Getenv("VALKEY_USERNAME"),
103-
Password: os.Getenv("VALKEY_PASSWORD"),
141+
Username: optEnvIndirect(opts, "valkey_username_env", "VALKEY_USERNAME"),
142+
Password: optEnvIndirect(opts, "valkey_password_env", "VALKEY_PASSWORD"),
104143
// The store contract is request/response; the client-side
105144
// cache would only add invalidation traffic.
106145
DisableCache: true,
@@ -170,12 +209,12 @@ func (s *Store) createIndex(dim int) error {
170209
if dim == 0 {
171210
return fmt.Errorf("valkey-store: Set: cannot index zero-length vectors")
172211
}
173-
algo := strings.ToUpper(os.Getenv("VALKEY_INDEX_ALGO"))
212+
algo := strings.ToUpper(s.indexAlgo)
174213
if algo == "" {
175214
algo = "FLAT"
176215
}
177216
if algo != "FLAT" && algo != "HNSW" {
178-
return fmt.Errorf("valkey-store: VALKEY_INDEX_ALGO %q not supported (FLAT or HNSW)", algo)
217+
return fmt.Errorf("valkey-store: valkey_index_algo/VALKEY_INDEX_ALGO %q not supported (FLAT or HNSW)", algo)
179218
}
180219

181220
ctx := context.Background()

backend/go/valkey-store/store_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,41 @@ var _ = Describe("StoresLoad namespace gate", func() {
3737
})
3838
})
3939

40+
var _ = Describe("model-config option resolution", func() {
41+
It("optString prefers the model option over the default", func() {
42+
opts := &pb.ModelOptions{Options: []string{"valkey_addr:10.0.0.1:6379"}}
43+
Expect(optString(opts, "valkey_addr", "127.0.0.1:6379")).To(Equal("10.0.0.1:6379"))
44+
})
45+
46+
It("optString falls back to the default when the option is absent", func() {
47+
Expect(optString(&pb.ModelOptions{}, "valkey_addr", "127.0.0.1:6379")).To(Equal("127.0.0.1:6379"))
48+
})
49+
50+
It("optEnvIndirect reads the env var named by the model option", func() {
51+
Expect(os.Setenv("VALKEY_STORE_TEST_CRED_A", "creds-a")).To(Succeed())
52+
defer func() { _ = os.Unsetenv("VALKEY_STORE_TEST_CRED_A") }()
53+
opts := &pb.ModelOptions{Options: []string{"valkey_password_env:VALKEY_STORE_TEST_CRED_A"}}
54+
Expect(optEnvIndirect(opts, "valkey_password_env", "VALKEY_PASSWORD")).To(Equal("creds-a"))
55+
})
56+
57+
It("optEnvIndirect falls back to the process-wide env var when unset", func() {
58+
Expect(os.Setenv("VALKEY_PASSWORD", "fallback-cred")).To(Succeed())
59+
defer func() { _ = os.Unsetenv("VALKEY_PASSWORD") }()
60+
Expect(optEnvIndirect(&pb.ModelOptions{}, "valkey_password_env", "VALKEY_PASSWORD")).To(Equal("fallback-cred"))
61+
})
62+
63+
It("lets two models pick different credential env vars against the same server", func() {
64+
Expect(os.Setenv("VALKEY_STORE_TEST_CRED_A", "creds-a")).To(Succeed())
65+
Expect(os.Setenv("VALKEY_STORE_TEST_CRED_B", "creds-b")).To(Succeed())
66+
defer func() { _ = os.Unsetenv("VALKEY_STORE_TEST_CRED_A") }()
67+
defer func() { _ = os.Unsetenv("VALKEY_STORE_TEST_CRED_B") }()
68+
a := &pb.ModelOptions{Options: []string{"valkey_username_env:VALKEY_STORE_TEST_CRED_A"}}
69+
b := &pb.ModelOptions{Options: []string{"valkey_username_env:VALKEY_STORE_TEST_CRED_B"}}
70+
Expect(optEnvIndirect(a, "valkey_username_env", "VALKEY_USERNAME")).To(Equal("creds-a"))
71+
Expect(optEnvIndirect(b, "valkey_username_env", "VALKEY_USERNAME")).To(Equal("creds-b"))
72+
})
73+
})
74+
4075
var _ = Describe("valkey-store against a live server", Ordered, func() {
4176
var nsCounter int
4277

@@ -99,6 +134,26 @@ var _ = Describe("valkey-store against a live server", Ordered, func() {
99134
Expect(s.Load(&pb.ModelOptions{Model: store.NamespacePrefix})).To(Succeed())
100135
DeferCleanup(func() { dropNamespace(s) })
101136
})
137+
138+
It("takes the index algorithm from the model option over VALKEY_INDEX_ALGO", func() {
139+
Expect(os.Setenv("VALKEY_INDEX_ALGO", "HNSW")).To(Succeed())
140+
defer func() { _ = os.Unsetenv("VALKEY_INDEX_ALGO") }()
141+
142+
nsCounter++
143+
ns := fmt.Sprintf("test-%d-%d", GinkgoRandomSeed(), nsCounter)
144+
s := NewStore()
145+
Expect(s.Load(&pb.ModelOptions{
146+
Model: store.NamespacePrefix + ns,
147+
Options: []string{"valkey_index_algo:FLAT"},
148+
})).To(Succeed())
149+
DeferCleanup(func() { dropNamespace(s) })
150+
Expect(s.indexAlgo).To(Equal("FLAT"), "model option should win over the env var")
151+
152+
mustSet(s, [][]float32{{1, 0, 0}}, [][]byte{[]byte("x")})
153+
info, err := s.client.Do(context.Background(), s.client.B().Arbitrary("FT.INFO").Args(s.index).Build()).ToAny()
154+
Expect(err).NotTo(HaveOccurred())
155+
Expect(fmt.Sprintf("%v", info)).To(ContainSubstring("FLAT"), "index should have been created with the option's algorithm, not the env var's")
156+
})
102157
})
103158

104159
Describe("StoresSet", func() {

docs/content/features/stores.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,30 @@ All endpoints also accept an optional `backend` field selecting which store back
5151
request. The default is `local-store`, an in-memory exact-search store. Alternatively,
5252
`valkey-store` (alias `valkey`) stores the vectors in a Valkey server running the
5353
[Valkey Search](https://valkey.io/topics/search/) module (the `valkey/valkey-bundle` image), which
54-
makes them durable across restarts. `valkey-store` is configured through environment variables:
55-
56-
| Variable | Default | Description |
57-
|---|---|---|
58-
| `VALKEY_ADDR` | `127.0.0.1:6379` | Valkey server address |
59-
| `VALKEY_USERNAME` | | Username, if the server requires AUTH |
60-
| `VALKEY_PASSWORD` | | Password, if the server requires AUTH |
61-
| `VALKEY_INDEX_ALGO` | `FLAT` | Vector index algorithm: `FLAT` (exact, matches `local-store` results) or `HNSW` (approximate, for large corpora) |
54+
makes them durable across restarts. `valkey-store` is configured through environment variables,
55+
or per-model via the model config's `options:` list, which takes priority over the env var:
56+
57+
| Variable | Model option | Default | Description |
58+
|---|---|---|---|
59+
| `VALKEY_ADDR` | `valkey_addr` | `127.0.0.1:6379` | Valkey server address |
60+
| `VALKEY_USERNAME` | `valkey_username_env` | | Username, if the server requires AUTH |
61+
| `VALKEY_PASSWORD` | `valkey_password_env` | | Password, if the server requires AUTH |
62+
| `VALKEY_INDEX_ALGO` | `valkey_index_algo` | `FLAT` | Vector index algorithm: `FLAT` (exact, matches `local-store` results) or `HNSW` (approximate, for large corpora) |
63+
64+
The username/password options name an environment variable to read the credential from, rather
65+
than taking the credential directly, so each model config can point at its own Valkey server with
66+
its own credentials without putting secrets in the YAML (the same pattern `cloud-proxy`'s
67+
`api_key_env` uses):
68+
69+
```yaml
70+
name: my-valkey-store
71+
backend: valkey-store
72+
options:
73+
- "valkey_addr:valkey.internal:6379"
74+
- "valkey_username_env:MY_VALKEY_USERNAME"
75+
- "valkey_password_env:MY_VALKEY_PASSWORD"
76+
- "valkey_index_algo:HNSW"
77+
```
6278
6379
## Set
6480

0 commit comments

Comments
 (0)