mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
refactor(stores): resolve the store backend and its loader slot explicitly
Store loading picked the backend and the loader's process slot in one function, which let an explicit `backend` argument collide with the store's own config and made traces report a fixed "local-store". This changes /stores/* behaviour for every store: - An explicit `backend` that differs from the one named in the store's model config now gets its own loader slot, `<store>@<backend>`, so it no longer reuses (or evicts) the process of the configured backend. Naming the configured backend, or one of its aliases, keeps the store's own slot. - When the config names a different backend than the explicit one, the config's `options` are not passed on, since they are meant for the configured backend. A config without `backend:` still passes its options to the explicit backend. - Backend names are compared and reported in canonical form (aliases resolved, lower case) through the new model.CanonicalBackend, and traces record the backend that was actually used instead of always "local-store". Backend loading itself uses CanonicalBackend for alias handling; type aliases keep their existing effect. Signed-off-by: Anush008 <mail@anush.sh> Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
1 parent
5aabc5891f
commit
dee4d8a399
3 files changed
+164
-51
No files matched your search
+56
-40
@@ -32,8 +32,8 @@ type Neighbor struct {
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// NewVectorStore returns a VectorStore backed by the local-store
|
||||
// gRPC backend, namespaced by storeName so two routers don't collide.
|
||||
// NewVectorStore returns a VectorStore backed by the store's gRPC backend,
|
||||
// namespaced by storeName so two routers don't collide.
|
||||
// cl resolves the per-store model config (backend + options); it may be nil,
|
||||
// in which case the store falls back to the default backend and its built-in
|
||||
// defaults.
|
||||
@@ -51,8 +51,8 @@ type localVectorStore struct {
|
||||
storeName string
|
||||
}
|
||||
|
||||
func (s *localVectorStore) backend(_ context.Context) (grpc.Backend, error) {
|
||||
return StoreBackend(s.loader, s.appConfig, s.cl, s.storeName, "")
|
||||
func (s *localVectorStore) backend(_ context.Context) (grpc.Backend, string, error) {
|
||||
return storeBackend(s.loader, s.cl, s.storeName, "")
|
||||
}
|
||||
|
||||
// Search is the top-1 special case of SearchK; delegating keeps the
|
||||
@@ -69,11 +69,11 @@ func (s *localVectorStore) Search(ctx context.Context, vec []float32) (float64,
|
||||
func (s *localVectorStore) SearchK(ctx context.Context, vec []float32, k int) (neighbors []Neighbor, err error) {
|
||||
outcome := "hit"
|
||||
sim := 0.0
|
||||
be, berr := s.backend(ctx)
|
||||
be, backendName, berr := s.backend(ctx)
|
||||
if berr != nil {
|
||||
outcome = "backend_load_error"
|
||||
err = fmt.Errorf("vector store load: %w", berr)
|
||||
s.recordTrace("", time.Now(), "search", len(vec), 0, outcome, err)
|
||||
s.recordTrace("", backendName, time.Now(), "search", len(vec), 0, outcome, err)
|
||||
return nil, err
|
||||
}
|
||||
release, err := AcquireGlobalBackendSlot()
|
||||
@@ -82,9 +82,9 @@ func (s *localVectorStore) SearchK(ctx context.Context, vec []float32, k int) (n
|
||||
}
|
||||
defer release()
|
||||
start := time.Now()
|
||||
traceID := s.beginTrace(start, "search")
|
||||
traceID := s.beginTrace(backendName, start, "search")
|
||||
defer func() {
|
||||
s.recordTrace(traceID, start, "search", len(vec), sim, outcome, err)
|
||||
s.recordTrace(traceID, backendName, start, "search", len(vec), sim, outcome, err)
|
||||
}()
|
||||
_, values, similarities, ferr := store.Find(ctx, be, vec, k)
|
||||
if ferr != nil {
|
||||
@@ -105,11 +105,11 @@ func (s *localVectorStore) SearchK(ctx context.Context, vec []float32, k int) (n
|
||||
|
||||
func (s *localVectorStore) Insert(ctx context.Context, vec []float32, payload []byte) (err error) {
|
||||
outcome := "ok"
|
||||
be, berr := s.backend(ctx)
|
||||
be, backendName, berr := s.backend(ctx)
|
||||
if berr != nil {
|
||||
outcome = "backend_load_error"
|
||||
err = fmt.Errorf("vector store load: %w", berr)
|
||||
s.recordTrace("", time.Now(), "insert", len(vec), 0, outcome, err)
|
||||
s.recordTrace("", backendName, time.Now(), "insert", len(vec), 0, outcome, err)
|
||||
return err
|
||||
}
|
||||
release, err := AcquireGlobalBackendSlot()
|
||||
@@ -118,9 +118,9 @@ func (s *localVectorStore) Insert(ctx context.Context, vec []float32, payload []
|
||||
}
|
||||
defer release()
|
||||
start := time.Now()
|
||||
traceID := s.beginTrace(start, "insert")
|
||||
traceID := s.beginTrace(backendName, start, "insert")
|
||||
defer func() {
|
||||
s.recordTrace(traceID, start, "insert", len(vec), 0, outcome, err)
|
||||
s.recordTrace(traceID, backendName, start, "insert", len(vec), 0, outcome, err)
|
||||
}()
|
||||
if serr := store.SetSingle(ctx, be, vec, payload); serr != nil {
|
||||
outcome = "insert_error"
|
||||
@@ -138,11 +138,11 @@ func (s *localVectorStore) InsertBatch(ctx context.Context, vecs [][]float32, pa
|
||||
if len(vecs) > 0 {
|
||||
dim = len(vecs[0])
|
||||
}
|
||||
be, berr := s.backend(ctx)
|
||||
be, backendName, berr := s.backend(ctx)
|
||||
if berr != nil {
|
||||
outcome = "backend_load_error"
|
||||
err = fmt.Errorf("vector store load: %w", berr)
|
||||
s.recordTrace("", time.Now(), "insert_batch", dim, 0, outcome, err)
|
||||
s.recordTrace("", backendName, time.Now(), "insert_batch", dim, 0, outcome, err)
|
||||
return err
|
||||
}
|
||||
release, err := AcquireGlobalBackendSlot()
|
||||
@@ -151,9 +151,9 @@ func (s *localVectorStore) InsertBatch(ctx context.Context, vecs [][]float32, pa
|
||||
}
|
||||
defer release()
|
||||
start := time.Now()
|
||||
traceID := s.beginTrace(start, "insert_batch")
|
||||
traceID := s.beginTrace(backendName, start, "insert_batch")
|
||||
defer func() {
|
||||
s.recordTrace(traceID, start, "insert_batch", dim, 0, outcome, err)
|
||||
s.recordTrace(traceID, backendName, start, "insert_batch", dim, 0, outcome, err)
|
||||
}()
|
||||
if serr := store.SetCols(ctx, be, vecs, payloads); serr != nil {
|
||||
outcome = "insert_error"
|
||||
@@ -171,11 +171,11 @@ func (s *localVectorStore) Delete(ctx context.Context, vecs [][]float32) (err er
|
||||
if len(vecs) > 0 {
|
||||
dim = len(vecs[0])
|
||||
}
|
||||
be, berr := s.backend(ctx)
|
||||
be, backendName, berr := s.backend(ctx)
|
||||
if berr != nil {
|
||||
outcome = "backend_load_error"
|
||||
err = fmt.Errorf("vector store load: %w", berr)
|
||||
s.recordTrace("", time.Now(), "delete", dim, 0, outcome, err)
|
||||
s.recordTrace("", backendName, time.Now(), "delete", dim, 0, outcome, err)
|
||||
return err
|
||||
}
|
||||
release, err := AcquireGlobalBackendSlot()
|
||||
@@ -184,9 +184,9 @@ func (s *localVectorStore) Delete(ctx context.Context, vecs [][]float32) (err er
|
||||
}
|
||||
defer release()
|
||||
start := time.Now()
|
||||
traceID := s.beginTrace(start, "delete")
|
||||
traceID := s.beginTrace(backendName, start, "delete")
|
||||
defer func() {
|
||||
s.recordTrace(traceID, start, "delete", dim, 0, outcome, err)
|
||||
s.recordTrace(traceID, backendName, start, "delete", dim, 0, outcome, err)
|
||||
}()
|
||||
if serr := store.DeleteCols(ctx, be, vecs); serr != nil {
|
||||
outcome = "delete_error"
|
||||
@@ -198,17 +198,16 @@ func (s *localVectorStore) Delete(ctx context.Context, vecs [][]float32) (err er
|
||||
// recordTrace surfaces vector-store calls in /api/backend-traces, including
|
||||
// the backend-load-failure path that otherwise vanishes into an xlog.Warn.
|
||||
// modelName uses the store namespace (e.g. "router-cache-smart-router") so
|
||||
// admins can tell which router's cache misbehaved; the backend is always
|
||||
// "local-store" and can't disambiguate.
|
||||
func (s *localVectorStore) beginTrace(start time.Time, op string) string {
|
||||
// admins can tell which router's cache misbehaved.
|
||||
func (s *localVectorStore) beginTrace(backendName string, start time.Time, op string) string {
|
||||
if s.appConfig == nil || !s.appConfig.EnableTracing {
|
||||
return ""
|
||||
}
|
||||
trace.InitBackendTracingIfEnabled(s.appConfig.TracingMaxItems, s.appConfig.TracingMaxBodyBytes)
|
||||
return trace.BeginBackendTrace(trace.BackendTrace{Timestamp: start, Type: trace.BackendTraceVectorStore, ModelName: s.storeName, Backend: model.LocalStoreBackend, Summary: op})
|
||||
return trace.BeginBackendTrace(trace.BackendTrace{Timestamp: start, Type: trace.BackendTraceVectorStore, ModelName: s.storeName, Backend: backendName, Summary: op})
|
||||
}
|
||||
|
||||
func (s *localVectorStore) recordTrace(traceID string, start time.Time, op string, vecDim int, sim float64, outcome string, err error) {
|
||||
func (s *localVectorStore) recordTrace(traceID, backendName string, start time.Time, op string, vecDim int, sim float64, outcome string, err error) {
|
||||
if s.appConfig == nil || !s.appConfig.EnableTracing {
|
||||
return
|
||||
}
|
||||
@@ -237,7 +236,7 @@ func (s *localVectorStore) recordTrace(traceID string, start time.Time, op strin
|
||||
Duration: time.Since(start),
|
||||
Type: trace.BackendTraceVectorStore,
|
||||
ModelName: s.storeName,
|
||||
Backend: model.LocalStoreBackend,
|
||||
Backend: backendName,
|
||||
Summary: summary,
|
||||
Error: errStr,
|
||||
Data: data,
|
||||
@@ -245,26 +244,43 @@ func (s *localVectorStore) recordTrace(traceID string, start time.Time, op strin
|
||||
}
|
||||
|
||||
func StoreBackend(sl *model.ModelLoader, appConfig *config.ApplicationConfig, cl *config.ModelConfigLoader, storeName string, backend string) (grpc.Backend, error) {
|
||||
// Resolve the per-store model config (keyed by the store namespace, which
|
||||
// is the model ID for a store). This is the LocalAI-native config surface:
|
||||
// a store's backend selection and its backend-specific settings live in a
|
||||
// model YAML's `backend:` and `options:` fields, so different stores can
|
||||
// point at different servers/indexes. When no config exists for the store,
|
||||
// we fall back to the default backend and let the backend apply its own
|
||||
// built-in defaults — preserving the zero-config experience.
|
||||
var loadOpts []string
|
||||
be, _, err := storeBackend(sl, cl, storeName, backend)
|
||||
return be, err
|
||||
}
|
||||
|
||||
func storeBackend(sl *model.ModelLoader, cl *config.ModelConfigLoader, storeName string, backend string) (grpc.Backend, string, error) {
|
||||
backend, loadOpts, modelID := resolveStoreBackend(cl, storeName, backend)
|
||||
be, err := loadStoreBackend(sl, modelID, storeName, backend, loadOpts)
|
||||
return be, model.CanonicalBackend(backend), err
|
||||
}
|
||||
|
||||
// The loader caches processes by ID alone, so an explicit backend other than
|
||||
// the store's own needs its own ID.
|
||||
func resolveStoreBackend(cl *config.ModelConfigLoader, storeName string, backend string) (string, []string, string) {
|
||||
home := model.LocalStoreBackend
|
||||
var cfgBackend string
|
||||
var opts []string
|
||||
if cl != nil {
|
||||
if cfg, ok := cl.GetModelConfig(storeName); ok {
|
||||
if backend == "" {
|
||||
backend = cfg.Backend
|
||||
cfgBackend, opts = cfg.Backend, cfg.Options
|
||||
if cfgBackend != "" {
|
||||
home = cfgBackend
|
||||
}
|
||||
loadOpts = cfg.Options
|
||||
}
|
||||
}
|
||||
|
||||
if backend == "" {
|
||||
backend = model.LocalStoreBackend
|
||||
backend = home
|
||||
}
|
||||
if model.CanonicalBackend(backend) == model.CanonicalBackend(home) {
|
||||
return backend, opts, storeName
|
||||
}
|
||||
if cfgBackend != "" {
|
||||
opts = nil
|
||||
}
|
||||
return backend, opts, storeName + "@" + model.CanonicalBackend(backend)
|
||||
}
|
||||
|
||||
func loadStoreBackend(sl *model.ModelLoader, modelID, storeName, backend string, loadOpts []string) (grpc.Backend, error) {
|
||||
// ModelLoader caches backend processes by `modelID`, not by the `model`
|
||||
// passed via WithModel. Without a distinct modelID, every StoreBackend
|
||||
// call collapses to the same `modelID=""` cache slot — face (512-D) and
|
||||
@@ -281,7 +297,7 @@ func StoreBackend(sl *model.ModelLoader, appConfig *config.ApplicationConfig, cl
|
||||
// convention upgrades in lockstep).
|
||||
sc := []model.Option{
|
||||
model.WithBackendString(backend),
|
||||
model.WithModelID(storeName),
|
||||
model.WithModelID(modelID),
|
||||
model.WithModel(store.NamespacePrefix + storeName),
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,36 @@ var _ = Describe("localVectorStore tracing", func() {
|
||||
Expect(bt.Error).To(ContainSubstring("vector store load"))
|
||||
})
|
||||
|
||||
It("records the backend selected by the store's model config, not local-store", func() {
|
||||
appCfg := &config.ApplicationConfig{
|
||||
EnableTracing: true,
|
||||
TracingMaxItems: 16,
|
||||
TracingMaxBodyBytes: 1024,
|
||||
}
|
||||
cl := config.NewModelConfigLoader(GinkgoT().TempDir())
|
||||
cl.ReplaceModelConfigs([]config.ModelConfig{
|
||||
{Name: "router-cache-valkey", Backend: "valkey"},
|
||||
})
|
||||
s := &localVectorStore{
|
||||
loader: model.NewModelLoader(&system.SystemState{}),
|
||||
appConfig: appCfg,
|
||||
cl: cl,
|
||||
storeName: "router-cache-valkey",
|
||||
}
|
||||
|
||||
err := s.Insert(context.Background(), []float32{0.1, 0.2}, []byte("payload"))
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
Eventually(func() *trace.BackendTrace {
|
||||
return findVectorStoreTrace("router-cache-valkey")
|
||||
}).ShouldNot(BeNil())
|
||||
|
||||
bt := findVectorStoreTrace("router-cache-valkey")
|
||||
Expect(bt.Backend).To(Equal(model.ValkeyStoreBackend))
|
||||
Expect(bt.Data["op"]).To(Equal("insert"))
|
||||
Expect(bt.Data["outcome"]).To(Equal("backend_load_error"))
|
||||
})
|
||||
|
||||
It("does not record a trace when tracing is disabled", func() {
|
||||
// Opt-out path: appConfig.EnableTracing=false must short-circuit
|
||||
// before InitBackendTracingIfEnabled, so a workload with tracing
|
||||
@@ -86,3 +116,70 @@ var _ = Describe("localVectorStore tracing", func() {
|
||||
}).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("resolveStoreBackend", func() {
|
||||
valkeyCfg := func(opts ...string) *config.ModelConfigLoader {
|
||||
cl := config.NewModelConfigLoader(GinkgoT().TempDir())
|
||||
cl.ReplaceModelConfigs([]config.ModelConfig{{
|
||||
Name: "faces",
|
||||
Backend: model.ValkeyStoreBackend,
|
||||
Options: opts,
|
||||
}})
|
||||
return cl
|
||||
}
|
||||
|
||||
It("defaults to local-store with no config and no explicit backend", func() {
|
||||
name, opts, id := resolveStoreBackend(nil, "some-store", "")
|
||||
Expect(name).To(Equal(model.LocalStoreBackend))
|
||||
Expect(opts).To(BeEmpty())
|
||||
Expect(id).To(Equal("some-store"))
|
||||
})
|
||||
|
||||
It("uses the store's model config backend and options", func() {
|
||||
name, opts, id := resolveStoreBackend(valkeyCfg("addr:valkey:6379"), "faces", "")
|
||||
Expect(name).To(Equal(model.ValkeyStoreBackend))
|
||||
Expect(opts).To(Equal([]string{"addr:valkey:6379"}))
|
||||
Expect(id).To(Equal("faces"))
|
||||
})
|
||||
|
||||
It("keeps the config's options and slot when the explicit backend is the configured one (aliases included)", func() {
|
||||
name, opts, id := resolveStoreBackend(valkeyCfg("addr:valkey:6379"), "faces", "valkey")
|
||||
Expect(name).To(Equal("valkey"))
|
||||
Expect(opts).To(Equal([]string{"addr:valkey:6379"}))
|
||||
Expect(id).To(Equal("faces"))
|
||||
})
|
||||
|
||||
It("gives an explicit override of another backend its own slot and not the config's options", func() {
|
||||
name, opts, id := resolveStoreBackend(valkeyCfg("addr:valkey:6379"), "faces", model.LocalStoreBackend)
|
||||
Expect(name).To(Equal(model.LocalStoreBackend))
|
||||
Expect(opts).To(BeEmpty())
|
||||
Expect(id).To(Equal("faces@local-store"))
|
||||
})
|
||||
|
||||
It("passes the options of a config without backend: to an explicit backend", func() {
|
||||
cl := config.NewModelConfigLoader(GinkgoT().TempDir())
|
||||
cl.ReplaceModelConfigs([]config.ModelConfig{{
|
||||
Name: "vectors",
|
||||
Options: []string{"addr:valkey:6379"},
|
||||
}})
|
||||
name, opts, id := resolveStoreBackend(cl, "vectors", model.ValkeyStoreBackend)
|
||||
Expect(name).To(Equal(model.ValkeyStoreBackend))
|
||||
Expect(opts).To(Equal([]string{"addr:valkey:6379"}))
|
||||
Expect(id).To(Equal("vectors@valkey-store"))
|
||||
})
|
||||
|
||||
It("separates an override from the default local-store slot when there is no config", func() {
|
||||
_, _, id := resolveStoreBackend(nil, "mix", "valkey")
|
||||
Expect(id).To(Equal("mix@valkey-store"))
|
||||
_, _, id = resolveStoreBackend(nil, "mix", "embedded-store")
|
||||
Expect(id).To(Equal("mix"))
|
||||
})
|
||||
|
||||
It("compares backends by canonical name (aliases, case)", func() {
|
||||
Expect(model.CanonicalBackend("valkey")).To(Equal(model.ValkeyStoreBackend))
|
||||
Expect(model.CanonicalBackend("Valkey")).To(Equal(model.ValkeyStoreBackend))
|
||||
Expect(model.CanonicalBackend("embedded-store")).To(Equal(model.LocalStoreBackend))
|
||||
Expect(model.CanonicalBackend(model.ValkeyStoreBackend)).To(Equal(model.ValkeyStoreBackend))
|
||||
Expect(model.CanonicalBackend("Valkey-Store")).To(Equal(model.ValkeyStoreBackend))
|
||||
})
|
||||
})
|
||||
+11
-11
@@ -37,6 +37,14 @@ var Aliases = map[string]string{
|
||||
"stablediffusion": StableDiffusionGGMLBackend,
|
||||
}
|
||||
|
||||
func CanonicalBackend(name string) string {
|
||||
b := strings.ToLower(name)
|
||||
if real, ok := Aliases[b]; ok {
|
||||
return real
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var TypeAlias = map[string]string{
|
||||
"sentencetransformers": "SentenceTransformer",
|
||||
"huggingface-embeddings": "SentenceTransformer",
|
||||
@@ -337,17 +345,9 @@ func (ml *ModelLoader) backendLoader(opts ...Option) (client grpc.Backend, err e
|
||||
"f16", opt.F16Memory)
|
||||
}
|
||||
|
||||
backend := strings.ToLower(o.backendString)
|
||||
if realBackend, exists := Aliases[backend]; exists {
|
||||
typeAlias, exists := TypeAlias[backend]
|
||||
if exists {
|
||||
xlog.Debug("alias is a type alias", "alias", backend, "realBackend", realBackend, "type", typeAlias)
|
||||
o.gRPCOptions.Type = typeAlias
|
||||
} else {
|
||||
xlog.Debug("alias", "alias", backend, "realBackend", realBackend)
|
||||
}
|
||||
|
||||
backend = realBackend
|
||||
backend := CanonicalBackend(o.backendString)
|
||||
if typeAlias, ok := TypeAlias[strings.ToLower(o.backendString)]; ok {
|
||||
o.gRPCOptions.Type = typeAlias
|
||||
}
|
||||
|
||||
modelFileName := o.modelFile
|
||||
|
||||
Reference in new issue
Block a user