Files
silo-server/internal/recommendations/search_query_vector.go
QuickandClaude Opus 4.8 ed6c084c68 feat(search): gate index events by active provider and harden rebuild reconcile
Completes the search-provider-interface wiring that the catalog hardening
commits already call into:

- Skip the transactional search-index-event write path when Meilisearch is
  not the active provider (ItemRepository.WithActiveSearchProvider /
  SearchIndexEventRepository.disabledByActiveProvider).
- Dead-letter catalog_search_index_events after 10 attempts instead of
  retrying forever.
- Track the rebuild high-water mark (MaxEventID / MarkProcessedThrough) and
  persist last_processed_event_id in UpdateStateAfterRebuild so a rebuild
  reconciles events enqueued during the rebuild.
- Validate (read-only) the embedding lock when embedding a search query
  instead of establishing/mutating it.
- Surface total_exact on the legacy /items browse response.
- Wire the active catalog search provider into the scanner and item repo at
  startup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 08:34:04 -04:00

58 lines
1.7 KiB
Go

package recommendations
import (
"context"
"fmt"
"strings"
)
// EmbedSearchQuery generates a canonical recommendation-space vector for a
// free-text catalog search query.
func (e *Engine) EmbedSearchQuery(ctx context.Context, query string) ([]float32, error) {
if e == nil || e.embClient == nil {
return nil, fmt.Errorf("recommendation embedding client is not configured")
}
query = strings.TrimSpace(query)
if query == "" {
return nil, nil
}
if strings.TrimSpace(e.cfg.EmbeddingBaseURL) == "" {
return nil, fmt.Errorf("recommendation embedding base URL is not configured")
}
if strings.TrimSpace(e.cfg.EmbeddingModel) == "" {
return nil, fmt.Errorf("recommendation embedding model is not configured")
}
if err := e.ensureEmbeddingLockConfig(ctx); err != nil {
return nil, err
}
vectors, err := e.embClient.Embed(ctx, []string{query})
if err != nil {
return nil, err
}
if len(vectors) == 0 || len(vectors[0]) == 0 {
return nil, fmt.Errorf("embedding API returned no query vector")
}
if err := e.validateQueryEmbeddingLock(ctx, vectors[0]); err != nil {
return nil, err
}
return ensureCanonicalDimensions(vectors[0])
}
func (e *Engine) validateQueryEmbeddingLock(ctx context.Context, vector []float32) error {
lock, err := e.repo.GetEmbeddingLock(ctx)
if err != nil {
return fmt.Errorf("load embedding lock: %w", err)
}
if lock == nil {
return nil
}
return validateQueryEmbeddingLock(lock, e.cfg.EmbeddingBaseURL, e.cfg.EmbeddingModel, len(vector))
}
func validateQueryEmbeddingLock(lock *EmbeddingLock, baseURL, model string, sourceDimensions int) error {
if lock == nil {
return nil
}
return lock.Validate(baseURL, model, sourceDimensions)
}