WWDC Quick Look 💓 By SwiftGGTeam
Support semantic search with Core Spotlight

Support semantic search with Core Spotlight

元の動画を見る

ハイライト

iOS 18 の Core Spotlight はセマンティック検索をネイティブサポート。ユーザーが類義語で検索してもターゲットにヒット——開発者が独自検索モデルを構築する必要はありません。


主要内容

Core Spotlight は iOS で app コンテンツをシステム Spotlight にインデックスする標準方式でした。しかし弱点がありました:検索は完全一致。ユーザーが「旅行」で検索してもタイトルが「出游」のメモは見つからず、「画像」では「写真」にマッチしません。Spotlight グローバル検索では多くの最適化がありましたが、app 内検索では完全一致がユーザーを苛立たせがち。多くの app が独自のあいまい検索やサードパーティライブラリを導入——開発コストは低くありません。

iOS 18 の Core Spotlight はセマンティック検索(semantic search)をネイティブサポート。ユーザーの検索語は Spotlight の query understanding モデルで処理され、「意味が近い」検索語でもターゲットにヒット。セマンティック検索はデフォルトで有効——追加設定不要、モデル訓練不要、Natural Language フレームワーク統合不要、バックエンド不要。コンテンツを正しく Spotlight に donate するだけで、app 内検索体験が飛躍的に向上します。

セッションは付帯インフラも紹介:batch indexing と client state で大量データのインデックスを効率化、新 CoreSpotlight Delegate Extension でデバイスアイドル時に re-index をトリガー、新 ranked results API で Spotlight と同様の Top Hits 表示、engagement signal でユーザー行動に応じて検索ランキングを継続最適化。すべてデバイスローカルで実行——インデックスデータはデバイス外に出ず、他 app もインデックス内容を見られません。

詳細

検索可能 Item の作成

第一に Spotlight に検索可能コンテンツを donate。CSSearchableItem 作成時に一意識別子、任意の domain 識別子、属性セットを提供(2:14):

// Creating searchable items for donation

let item = CSSearchableItem(uniqueIdentifier: uniqueIdentifier, domainIdentifier: domainIdentifier, attributeSet: attributeSet)
  • uniqueIdentifier は app の永続化スキームに保存し、後で完全データを復元
  • domainIdentifier は任意で、検索可能 item のグループ管理に使用

次に CSSearchableAttributeSet を作成し、有効な contentType を設定(2:28):

// Creating searchable content for donation

let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.text)
attributeSet.contentType = UTType.text.identifier
  • contentType がセマンティックインデックスの処理方法を決定
  • テキスト item には titletextContent を設定——両方がセマンティックインデックスで処理
  • 画像/動画 item には contentURL でリソースファイルパスを設定
  • 添付や Web コンテンツは独立 item として donate し、relatedUniqueIdentifier でソース item と関連付け

Batch Indexing と Client State

大量データのインデックスには iOS 18 の batch indexing + client state(3:31):

// Batch indexing with client state

let index = CSSearchableIndex(name: "SpotlightSearchSample")
index.fetchLastClientState { state, error in
    if state == nil {
        index.beginBatch()
        index.indexSearchableItems(items)
        index.endIndexBatch(expectedClientState: state, newClientState: newState) { error in
        }
    }
}
  • 名前付き CSSearchableIndex を作成し fetchLastClientState で前回インデックス状態を取得
  • beginBatch()endIndexBatch() でインデックス操作をラップ、newClientState で進捗を記録
  • client state で重複 donate を防止しパフォーマンス問題を回避
  • isUpdate フラグで必要な更新のみ donate(3:56):
// Make it an update to avoid overwriting existing attributes

item.isUpdate = true

CoreSpotlight Delegate Extension

Spotlight がインデックス移行やデータ破損からの復旧を必要とする場合、app に re-index を要求。iOS 18 の新 CoreSpotlight Delegate Extension テンプレートで、デバイスアイドル時(スリープや未使用時)に re-index 要求をスケジュールし、段階的にデータ移行——既存検索機能への影響を最小化。

設定:Xcode で File > New > Target、「CoreSpotlight Delegate extension」テンプレートを検索。デバッグは mdutil コマンドラインツールで bundle ID への re-index 要求をシミュレート。

クエリ設定とセマンティック検索

セマンティック検索はデフォルト有効。CSUserQueryContext でクエリを設定(7:19):

// Configure a query

let queryContext = CSUserQueryContext()
queryContext.fetchAttributes = ["title", "contentDescription"]
  • fetchAttributes は UI 表示に必要な属性のみ設定し、不要なデータ読み取りを削減

ranked results を有効化し Top Hits を表示(7:33):

// Ranked results

queryContext.enableRankedResults = true
queryContext.maxRankedResultCount = 2
  • ranked results は Spotlight と同じ機械学習モデルでソート
  • 結果返却後は compareByRank ソーターでソート

検索候補を設定(7:47):

// Suggestions

queryContext.maxSuggestionCount = 4

filter queries でコンテンツタイプをフィルタ(7:55):

// Filter queries

queryContext.filterQueries = ["contentTypeTree=\"public.image\""]
  • filter queries は metadata 構文で特定タイプのみに結果を限定
  • タブ切替(画像のみなど)やパンくずナビに適する

クエリ実行と結果処理

CSUserQuery で検索を実行(8:23):

// Query for searchable items and suggestions

let query = CSUserQuery(userQueryString: "windsurfing carmel", userQueryContext: queryContext)
for try await element in query.responses {
    switch(element) {
        case .item(let item):
            self.items.append(item)
            break
        case .suggestion(let suggestion):
            self.suggestions.append(suggestion)
            break
    }
}
  • CSUserQuery はユーザー入力の検索文字列と query context を受け取る
  • 結果は async responses で返却、item と suggestion を分けて処理
  • suggestion は localizedAttributedSuggestion で表示
  • ユーザーが suggestion をタップしたら検索バー内容を suggestion テキストに置換し新検索をトリガー

セマンティック検索は ML モデルをデバイスにダウンロードし app プロセス内で実行。メモリ節約のためモデルは随時ロード/アンロード。検索 UI 表示前に毎回 CSUserQuery.prepare を呼びリソースを確保(8:56):

// Preparing for queries

CSUserQuery.prepare
CSUserQuery.prepareWithProtectionClasses

Engagement Signal でランキング向上

ユーザーがコンテンツを閲覧したり検索結果をタップしたりすると、app は engagement signal を Spotlight に送信し将来の検索ランキングを最適化(9:50):

// Set the lastUsedDate when the user interacts with the item

item.attributeSet.lastUsedDate = Date.now
item.isUpdate = true

検索結果や候補とのインタラクション時にマーク(10:00):

// Interactions with items and suggestions from a query

query.userEngaged(item, visibleItems: visibleItems, interaction: CSUserQuery.UserInteractionKind.select)

query.userEngaged(suggestion, visibleSuggestions: visibleSuggestions, interaction: CSUserQuery.UserInteractionKind.select)
  • lastUsedDate は freshness シグナル——ユーザーが最近 item を使用
  • userEngaged は engagement シグナル——どの結果がタップされたか、当時表示されていた他の結果リストを含む
  • 両シグナルが Spotlight ランキングモデルのユーザー嗜好学習を支援

重要ポイント

  • 何をするか:既存 Core Spotlight 統合に ranked results と検索候補を追加。なぜ価値があるか:ranked results は Spotlight と同じ ML ソートモデルで Top Hits エリアを直接表示——検索体験が「リスト羅列」から「スマート推薦」へ。始め方:既存 CSUserQueryContextenableRankedResults = truemaxRankedResultCount を設定、返却後 compareByRank でソート。

  • 何をするか:逐次インデックスを batch indexing + client state に置換。なぜ価値があるか:client state が前回インデックス進捗を記録し、app 再起動や Spotlight re-index 要求時に断点再開——全量再 donate によるパフォーマンス問題を回避。始め方:名前付き CSSearchableIndex を作成、fetchLastClientState で状態確認、beginBatch / endIndexBatch 間で一括インデックス、カーソルを newClientState に保存。

  • 何をするか:検索結果タップ時に engagement signal を送信。なぜ価値があるか:Spotlight ランキングモデルがユーザー行動から継続学習——よくタップされる item は将来の検索で上位に。始め方:コンテンツ閲覧時に lastUsedDate を設定し isUpdate = true でインデックスに書き戻し;検索結果タップ時に query.userEngaged() を呼び、タップ item と当時表示されていた結果リストを渡す。

  • 何をするか:CoreSpotlight Delegate Extension でインデックス復旧をサポート。なぜ価値があるか:システムアップグレードやデータ破損時に Spotlight インデックス再構築が必要——delegate extension なしでは検索が失効し、ユーザーが手動で app 再起動するまで再インデックス不可。始め方:Xcode で CoreSpotlight Delegate extension テンプレートから target 作成、reindexAllItemsreindexItemsWithIdentifiers を実装、mdutil でデバッグ。

関連セッション

コメント

GitHub Issues · utterances