feat(search): expose active embedding model from lock
Add recommendations.Engine.ActiveEmbeddingModel, which reads the embedding lock from server_settings and returns the locked model (or "" when no lock is established). Define the catalog CatalogSemanticModelProvider interface so a later task can scope vector coverage checks to the active model; the recommendations Engine already satisfies it. No wiring yet (Task 4 injects recEngine as this provider). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,13 @@ type CatalogSearchQueryVectorizer interface {
|
||||
EmbedSearchQuery(ctx context.Context, query string) ([]float32, error)
|
||||
}
|
||||
|
||||
// CatalogSemanticModelProvider reports the embedding model currently locked for
|
||||
// this installation. Task 4 injects the recommendations engine as this provider
|
||||
// so catalog vector-coverage checks can be scoped to the active model.
|
||||
type CatalogSemanticModelProvider interface {
|
||||
ActiveEmbeddingModel(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type CatalogSearchCandidateRetriever interface {
|
||||
CandidateIDs(ctx context.Context, vector []float32, itemTypes []string, limit int) ([]string, error)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,19 @@ func NewEngine(
|
||||
}
|
||||
}
|
||||
|
||||
// ActiveEmbeddingModel returns the embedding model currently locked for this
|
||||
// installation, or "" when no lock is established.
|
||||
func (e *Engine) ActiveEmbeddingModel(ctx context.Context) (string, error) {
|
||||
lock, err := e.repo.GetEmbeddingLock(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if lock == nil {
|
||||
return "", nil
|
||||
}
|
||||
return lock.Model, nil
|
||||
}
|
||||
|
||||
func (e *Engine) watchedItemIDSet(ctx context.Context, userID int, profileID string) (map[string]struct{}, error) {
|
||||
return e.signalReader().WatchedItemIDSet(ctx, userID, profileID)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package recommendations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/Silo-Server/silo-server/internal/catalog"
|
||||
)
|
||||
|
||||
// Compile-time assertions that *Engine satisfies both catalog provider
|
||||
// interfaces it will be injected as. CatalogSearchQueryVectorizer is the
|
||||
// existing query-embedding hook; CatalogSemanticModelProvider is added in this
|
||||
// task and wired in Task 4.
|
||||
var (
|
||||
_ catalog.CatalogSearchQueryVectorizer = (*Engine)(nil)
|
||||
_ catalog.CatalogSemanticModelProvider = (*Engine)(nil)
|
||||
)
|
||||
|
||||
// newEngineTestPool mirrors internal/catalog/display_query_filter_test.go: it
|
||||
// skips when SILO_TEST_DATABASE_URL is unset and verifies the base schema is
|
||||
// present before returning a usable pool.
|
||||
func newEngineTestPool(t *testing.T) *pgxpool.Pool {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("SILO_TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("SILO_TEST_DATABASE_URL is not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("connect test database: %v", err)
|
||||
}
|
||||
t.Cleanup(pool.Close)
|
||||
var tableName *string
|
||||
if err := pool.QueryRow(ctx, `SELECT to_regclass('public.server_settings')::text`).Scan(&tableName); err != nil {
|
||||
t.Fatalf("check server_settings table: %v", err)
|
||||
}
|
||||
if tableName == nil || *tableName == "" {
|
||||
t.Skip("test database has not applied base schema")
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// snapshotEmbeddingLock captures the current embedding lock row (if any) and
|
||||
// registers cleanup that restores the original state, so these tests do not
|
||||
// leave behind or clobber a real lock.
|
||||
func snapshotEmbeddingLock(t *testing.T, pool *pgxpool.Pool) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
var original *string
|
||||
err := pool.QueryRow(ctx,
|
||||
`SELECT value FROM server_settings WHERE key = $1`,
|
||||
embeddingLockSettingKey,
|
||||
).Scan(&original)
|
||||
if err != nil {
|
||||
original = nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
cctx := context.Background()
|
||||
if original == nil {
|
||||
_, _ = pool.Exec(cctx, `DELETE FROM server_settings WHERE key = $1`, embeddingLockSettingKey)
|
||||
return
|
||||
}
|
||||
_, _ = pool.Exec(cctx, `
|
||||
INSERT INTO server_settings (key, value) VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||
`, embeddingLockSettingKey, *original)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEngineActiveEmbeddingModelNoLock(t *testing.T) {
|
||||
pool := newEngineTestPool(t)
|
||||
snapshotEmbeddingLock(t, pool)
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := pool.Exec(ctx, `DELETE FROM server_settings WHERE key = $1`, embeddingLockSettingKey); err != nil {
|
||||
t.Fatalf("clear embedding lock: %v", err)
|
||||
}
|
||||
|
||||
e := &Engine{repo: NewRepo(pool)}
|
||||
model, err := e.ActiveEmbeddingModel(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ActiveEmbeddingModel returned error: %v", err)
|
||||
}
|
||||
if model != "" {
|
||||
t.Fatalf("ActiveEmbeddingModel = %q, want empty string when no lock", model)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineActiveEmbeddingModelWithLock(t *testing.T) {
|
||||
pool := newEngineTestPool(t)
|
||||
snapshotEmbeddingLock(t, pool)
|
||||
ctx := context.Background()
|
||||
|
||||
repo := NewRepo(pool)
|
||||
if err := repo.SetEmbeddingLock(ctx, EmbeddingLock{
|
||||
BaseURL: "http://x",
|
||||
Model: "test-model-x",
|
||||
SourceDimensions: CanonicalEmbeddingDimensions,
|
||||
}); err != nil {
|
||||
t.Fatalf("SetEmbeddingLock: %v", err)
|
||||
}
|
||||
|
||||
e := &Engine{repo: repo}
|
||||
model, err := e.ActiveEmbeddingModel(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ActiveEmbeddingModel returned error: %v", err)
|
||||
}
|
||||
if model != "test-model-x" {
|
||||
t.Fatalf("ActiveEmbeddingModel = %q, want %q", model, "test-model-x")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user