Skip to content

feat(search): rank search results by semantic similarity#67

Draft
bilipp wants to merge 1 commit into
mainfrom
feat/semantic-search
Draft

feat(search): rank search results by semantic similarity#67
bilipp wants to merge 1 commit into
mainfrom
feat/semantic-search

Conversation

@bilipp

@bilipp bilipp commented Jun 15, 2026

Copy link
Copy Markdown
Owner

What

Search now actually uses the on-device NLContextualEmbedding vectors that the background ContentIndexer already stores in Movie.embeddingData / Series.embeddingData. Until now those vectors were generated and persisted but never readTextEmbedder.decode had no callers and search was pure substring matching.

How

SemanticSearchService (new actor, singleton like ContentIndexingService)

  • Lazily creates and prepare()s a TextEmbedder, reused across searches. If the model can't load on the device, it caches that fact and returns nil so search degrades gracefully — no repeated OTA download attempts.
  • Embeds the query, then on a background ModelContext fetches every indexed title (embeddingData != nil, propertiesToFetch to avoid faulting whole rows), decodes each vector, and cosine-ranks with Accelerate (vDSP).
  • Returns ordered (id, score) lists above a 0.35 similarity threshold, capped per content type.

SearchView — now hybrid:

  • The existing localizedStandardContains (lexical) pass still runs and always leads the results, so exact-title hits, live channels (never embedded), and not-yet-indexed titles always appear.
  • Semantic-only matches are appended, deduped against the lexical set, and ordered by score across movies + series.
  • Staleness-guarded after the await so debounced/cancelled queries don't publish stale results.

LumeApp — configures the service with the shared container at launch, alongside the indexer.

Behavior

  • "Inception" → still top, via lexical match.
  • "heist inside a dream" / "movie about robots falling in love" → now surfaces conceptually-related titles whose names don't contain those words.
  • Same 300ms debounce; the heavy scan runs off the main thread.

Testing

  • ✅ Builds clean on the iOS 26.5 simulator.
  • ✅ Pre-commit gates (swiftformat + swiftlint) pass.
  • ⚠️ Not runtime-verified for ranking quality — needs a library that's been through the background indexer (which downloads the embedding model OTA). The 0.35 threshold may want tuning against a real catalog.
  • No unit tests added: ranking depends on the on-device ML model (non-deterministic to set up) and the vector math is private to the actor.

🤖 Generated with Claude Code

Search now consumes the NLContextualEmbedding vectors the background
ContentIndexer already stores in Movie/Series.embeddingData, which were
generated but never read.

- SemanticSearchService: actor that embeds the query and cosine-ranks the
  stored vectors off the main thread (Accelerate/vDSP), on a background
  ModelContext. Falls back silently to lexical-only when the embedding
  model can't load on the device.
- SearchView: hybrid results — the existing localizedStandardContains pass
  always leads (covers exact titles, live channels, and not-yet-indexed
  items), then semantic-only matches are appended, deduped and ranked by
  score. Staleness-guarded after the await.
- LumeApp: configure the service with the shared container at launch.
@bilipp bilipp force-pushed the feat/semantic-search branch 2 times, most recently from e69725a to f921d5d Compare June 17, 2026 13:58
@bilipp bilipp marked this pull request as draft July 1, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant