用 Core Spotlight 实现语义搜索
Support semantic search with Core Spotlight
2024年6月10日
一句话判断
Core Spotlight 今年加入了语义搜索(semantic search),用户不再需要精确匹配关键词,用自然语言就能搜到你 app 里的内容——而且实现成本极低。
这场 Session 讲了什么
Core Spotlight 一直是 iOS 上让 app 内容被系统 Spotlight 索引的标准方式。但一直以来它有一个明显的短板:搜索是精确匹配的。用户搜”旅行”找不到标题写”出游”的笔记,搜”图片”匹配不到”照片”。这在 Spotlight 全局搜索中体验还行(因为苹果自己做了不少优化),但在 app 内搜索场景下,精确匹配经常让用户抓狂。很多 app 为了解决这个问题,不得不自己做一套模糊搜索逻辑,或者引入第三方搜索库,开发成本不低。
iOS 18 的 Core Spotlight 原生支持了语义搜索。用户输入的搜索词会被 Spotlight 的 query understanding 模型处理,“意思相近”的搜索词也能命中目标。这意味着你只需要把内容正确地 donate 给 Spotlight,app 内的搜索体验就能直接获得质的飞跃——不需要自己训练模型,不需要集成 Natural Language 框架,不需要搭后端。语义搜索默认就是开启的,你不需要写额外的配置代码。
Session 还介绍了配套的基础设施改进:batch indexing 配合 client state 让大批量数据的索引更高效,新的 CoreSpotlight Delegate Extension 让 Spotlight 能在设备空闲时触发 re-index,新的 ranked results API 让你能像 Spotlight 一样展示 Top Hits。
这一切都跑在设备本地,索引数据从不离开设备,其他 app 也无法看到你的索引内容。隐私方面,苹果延续了 Core Spotlight 一贯的本地化策略——语义搜索的 embedding 和推理都在端上完成,不需要联网。对于在搜索体验上投入了大量精力但一直受限于精确匹配的开发者来说,这次更新算是一次”不用改架构就能大幅提升体验”的好机会。
值得深挖的点
语义搜索的生效条件:你必须正确设置 content type
语义搜索不是无脑生效的。它目前对文本(title + textContent)和媒体资产(图片、视频,通过 contentURL 指向)效果最好。如果你创建 CSSearchableItem 时不设置 contentType,或者用了不正确的 UTType,语义索引根本不会处理你的数据。
这里有一个容易踩的坑:如果你之前用 Core Spotlight 只做了最小实现(设置了 identifier 和 title 就完事了),语义搜索对你不起作用。你需要回头确保每个 item 都有一个合理的 contentType,文本内容要同时设置 title 和 textContent,图片/视频要通过 contentURL 让索引器能访问到实际文件。这些属性会被送入语义索引管线进行处理。Session 的示例 app 是一个日记应用——每篇日记作为一个 searchable item,设置了 contentType 为 UTType.plainText,同时设置了 title 和 textContent。这样语义搜索就能理解日记的内容,即使用户搜”吃饭”也能找到写”聚餐”的日记。
另一个容易被忽略的点是 relatedUniqueIdentifier——如果你的 item 有关联的附件或网页内容,应该把它们作为独立的 searchable item 捐赠,用 relatedUniqueIdentifier 建立关系。这样做的好处是关联内容也会被语义索引,搜索覆盖面更广。
Ranked Results:和 Spotlight 一样的排序能力
Session 引入了 ranked results,背后的排序模型就是 Spotlight 自己用的那套机器学习模型。通过 CSUserQueryContext 配置,你可以拿到按相关性排序的结果列表,还能限制返回数量,非常适合用来做”Top Hits”区域。
let queryContext = CSUserQueryContext()
queryContext.fetchedAttributes = ["title", "textContent"]
queryContext.rankedResults = true // 启用排序
queryContext.maximumRankedResultCount = 5 // Top 5
这个 API 的价值在于:以前你想在 app 内做”智能排序”需要自己搞一套 scoring 逻辑,现在直接调用 Spotlight 的能力就行。而且由于是设备端模型,用户数据不会离开设备,隐私性有保障。
除了 ranked results,Session 还重点介绍了 CoreSpotlight Delegate Extension。这是一个独立的 app extension,Spotlight 可以在 app 不运行的时候(比如设备空闲或充电时)调用它来执行 re-index。这对索引一致性至关重要——你的 Spotlight 索引是纯本地的,如果数据损坏或系统升级导致索引丢失,没有这个 extension,Spotlight 只能等到你的 app 下次启动才能恢复索引。有了 extension,Spotlight 可以在合适的时机主动修复。Xcode 今年新增了 CoreSpotlight Delegate Extension 模板,创建过程简化到几步点击。调试时可以用 mdutil 命令行工具模拟 Spotlight 的 re-index 请求,在 breakpoint 中观察整个恢复流程。
代码片段
正确捐赠 searchable item(语义搜索的前提)
let attributeSet = CSSearchableAttributeSet(
contentType: UTType.plainText // 必须设置正确的 content type
)
attributeSet.title = "周末出游计划"
attributeSet.textContent = "这周六去西湖骑行,带上相机和三明治"
// title 和 textContent 都会被送入语义索引
let item = CSSearchableItem(
uniqueIdentifier: "entry-123",
domainIdentifier: "journal-entries",
attributeSet: attributeSet
)
let index = CSSearchableIndex(name: "journalIndex")
try await index.index([item])
场景:让 app 的文本内容被语义搜索覆盖。坑:如果 textContent 为空或太短(只有几个字),语义搜索的效果会大打折扣。
使用 client state 做增量索引
let index = CSSearchableIndex(name: "journalIndex")
// 启动时验证上次的状态
if let lastState = index.lastClientState {
// 根据 state 判断哪些需要更新
}
try index.beginBatch()
try index.indexSearchableItems(updatedItems) // 只索引变化的
try index.endBatch(withClientState: newState) // 保存进度
场景:app 有大量可搜索内容,每次全量索引太慢。坑:lastClientState 返回 nil 不代表没索引过,可能是首次安装或 Spotlight 重置了索引。
CoreSpotlight Delegate Extension 处理 re-index
class SpotlightDelegate: CSIndexDelegate {
func searchableIndex(
_ searchableIndex: CSSearchableIndex,
reindexAllSearchableItemsWithAcknowledgementHandler
acknowledgementHandler: @escaping () -> Void
) {
// 重新索引所有内容
Task {
let items = try await fetchAllItems()
try searchableIndex.indexSearchableItems(items)
acknowledgementHandler() // 必须调用,否则会阻塞
}
}
}
场景:Spotlight 索引损坏或系统升级时自动触发恢复。坑:不调用 acknowledgementHandler 会导致 Spotlight 反复重试,严重影响性能。
最佳实践
新项目:从一开始就把 Core Spotlight 集成进数据层。每次创建/更新/删除内容时同步捐赠 searchable item,用 client state 跟踪索引进度。搜索界面用 CSUserQueryContext 配置语义搜索 + ranked results,直接获得 Spotlight 级别的搜索体验。
已有项目:如果你已经用了 Core Spotlight,检查现有 item 是否设置了 contentType、title、textContent。如果没有,加上这些属性就能让语义搜索生效。然后用 Xcode 新的 CoreSpotlight Delegate Extension 模板添加索引恢复支持。调试时用 mdutil 命令行工具模拟 re-index 请求。
还有什么值得关注
- 新的
isUpdateflag 可以避免重复索引已有内容,对于频繁更新的数据源很有用。 - 建议类(suggestions)API 支持在搜索框下方展示补全建议,构建完整的搜索 UI 更方便了。
- 整个语义搜索的处理完全在设备端完成,搜索数据不会离开设备,隐私保护一如既往。