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>
58 lines
1.7 KiB
Go
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)
|
|
}
|