iOS Search APIs

Introduce

iOS 9 之后,Apple 开放了三大 Search APIs,以便用户能在 Spotlight、Safari 等搜索入口,搜索到应用中的内容,这十分有助于提升应用的用户活跃度。

搜索入口

这三组 Search APIs 分别是:

  • NSUserActivity:能够为用户在 App 中的历史活动建立索引,例如到达某个关键页面,或是浏览到某个内容页。以便以后用户能够通过搜索结果恢复到该页面。
  • Core Spotlight:为 App 中的一些重要内容,在设备上建立索引,以提供快速入口。
  • Web markup:关联服务器上的内容到搜索结果中,用户即使没有安装 App,也能在搜索结果中看到相关内容。

本文主要介绍前两种 API —— NSUserActivity 和 Core Spotlight。

Note: 虽然全局搜索(app search)功能在 iOS 9 后就可用了,但是实际上,部分旧机型仍然不支持 NSUserActivity 和 Core Spotlight 的搜索功能,如 iPhone 4s、iPad 2、iPad(第三代)、iPad mini,以及 iPod touch 5.

Core Spotlight

Index App Content

Core Spotlight 主要用于为 App 中的一些重要内容(比较静态),在设备上建立索引。例如,应用的一些常用功能页面,以及一些文档、音视频内容,以便用户能够很方便地访问这些常用内容。

Core Spotlight

使用前需要 import Core Spotlight

1、建立索引

索引对应的类是CSSearchableItem。建立索引前,要先创建索引的属性集CSSearchableItemAttributeSet

CSSearchableItemAttributeSet中包含了大量的属性,包括音视频信息、文件信息、出版信息、联系人信息等等。实例化属性集时,就要指定属性集的类型,如kUTTypeImage,这会影响到索引的显示样式。这些常量声明于MobileCoreServices.UTCoreTypes中,因此需要 import 这个头文件。属性集中比较常用的属性为 title、contentDescription 和 keywords,title 就是索引显示的标题,contentDescription 是对内容的描述,keywords 则是索引的关键词数组,官方建议关键词为 5 个左右。需要注意的是,title 默认就能被搜索到,不需要加入 keywords。

设置完索引的属性集之后,就可以通过CSSearchableIteminit(uniqueIdentifier: domainIdentifier: attributeSet:)来实例化一个索引。其中,参数uniqueIdentifier是该索引的唯一标识符,可以通过这个标识符来删除这条索引;domainIdentifier是索引所在的域名标识,域名标识可以用于将索引分类存放,可以根据它来删除同一个domainIdentifier下的所有索引。

接下来便是建立索引。建立索引需要调用CSSearchableIndex实例的indexSearchableItems()方法。它能够批量建立索引,并提供一个回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func createSearchIndex(with followers: [User]) {
if #available(iOS 9.0, *) {
var searchableItems = [CSSearchableItem]()
users.forEach { (follower) in
// 索引的属性集(只用于控制索引在被搜索时的属性,不能存储数据)
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeImage as String)
// 索引的标题(标题也能被搜索到)
attributeSet.title = follower.name
// 索引的描述
attributeSet.contentDescription = "我的爱豆"
// 用于建立索引的关键词
attributeSet.keywords = ["punmy", "爱豆", follower.identifier]
// Spotlight 索引的类
let searchableItem = CSSearchableItem(uniqueIdentifier: "punmy://user?id=\(follower.identifier)", domainIdentifier: "com.meitu.yy.followers", attributeSet: attributeSet)
searchableItems.append(searchableItem)
}
CSSearchableIndex.default().indexSearchableItems(searchableItems, completionHandler: { (error) in
if let error = error {
DDLogError("Create searchable index failed, error: \(error.localizedDescription)")
}
})
}
}

func cleanSearchIndex() {
if #available(iOS 9.0, *) {
// 删除整个域名标识下的索引
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["com.punmy.followers"]) { (error) in
if let _ = error {
DDLogError("delete searchable items failed, error: \(error?.localizedDescription)")
}
}
}
}

调用indexSearchableItems()方法成功后,就能立即在 Spotlight 中搜索到刚刚添加的索引了。

Spotlight 搜索

通过 CSSearchableIndex 实例的几个删除方法,就能部分,或者全部删除索引。

2、实现回调

要处理从搜索结果中进入 App 的回调事件,需要在 AppDelegate 中实现application:continueUserActivity:restorationHandler:方法。这个回调在许多地方都会用到,如 Core Spotlight、Siri Kit、Handoff 等。可以在这个页面中,利用 userActivityuserInfo 字典中的数据,跳转到相应的页面。

Note:值得注意的是,之前我们为userActivity所设置的属性集,在这里都不存在了。只有保存在 userInfo 中的数据,才能在这个回调中获取到。

之前创建CSSearchableItem时传入的uniqueIdentifier参数,在此处可以通过 userInfo 中的 CSSearchableItemActivityIdentifier key值读取到,可以参考一下代码。

1
2
3
4
5
6
7
8
9
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
if #available(iOS 9.0, *) {
if let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
// 跳转到对应页面
restoreActivity(scheme: identifier)
}
}
return true
}

上文我们已经提到过,有许多功能的回调入口都是这个方法,那么如何区分不同入口呢?
一种办法是像上方代码一样,判断userInfo中是否存在 key 为CSSearchableItemActivityIdentifier的值;另一种办法可以判断userActivityactivityType属性的值,是否为CSSearchableItemActionType

在某些功能中,userActivity 的 activityType 是设置为自定义域名的,如下面将会讲到 NSUserActivity 部分。但在 Core Spotlight 中,所有的索引的类型都被系统设置为CSSearchableItemActionType

NSUserActivity

Index Activities and Navigation Points

NSUserActivity 主要用于为用户在 App 中的历史活动建立索引。NSUserActivity 的作用有些类似于浏览器中的“历史记录”,只是记录的内容是由我们来决定的。NSUserActivity 可以配合 Handoff 一起使用,以便在其他设备上继续当前活动,有关 Handoff 可以参照:Handoff 介绍

历史活动

我们可以在用户使用 App 的时候,将他浏览过的一些关键节点,或是重要的内容页面,在系统中建立一个索引,保存必要的数据。日后用户在 Spotlight 等地方搜索相关内容时,就能在搜索结果中显示该活动记录(UserActivity)。当用户点击该搜索结果时,系统会调起 App,并传入之前保存的数据,应用就能以此来恢复现场。

1、建立索引

首先,我们需要在用户浏览到某些页面的时候,将该页面加入系统索引。例如在 viewDidLoad()的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// MARK: - Indexing activity

let user
var activity? // 务必对 Activity 进行强引用

override func viewDidLoad() {
super.viewDidLoad()

let activity = NSUserActivity(activityType: "com.meitu.punmy.user")
// 索引的标题
activity.title = user.name
if #available(iOS 9.0, *) {
// 索引的属性集(只影响搜索结果的显示样式,不能用于存储数据)
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeContact as String)
// 索引的描述
attributeSet.contentDescription = user.desc
// 能被搜索到的关键词(索引的标题也能被搜索到)
attributeSet.keywords = [user.identifier, "punmy"]
// 与项目本身的 scheme 搭配使用,便于跳转
let scheme = "punmy://user?id=\(user.identifier)"
// 关联已有的 SpotLight Item
attributeSet.relatedUniqueIdentifier = scheme

activity.contentAttributeSet = attributeSet

// 只有 userInfo 中能够保存数据
activity.userInfo = ["scheme": scheme]
// 是否建立索引
activity.isEligibleForSearch = true
// 是否支持 Handoff 功能
activity.isEligibleForHandoff = false
// 索引的过期时间,默认一个月
activity.expirationDate = Date(timeIntervalSinceNow: 7 * 24 * 60 * 60)
}
// 激活 Activity
activity.becomeCurrent()
// 必须对 Activity 进行强引用,否则 Activity 很可能不会被加入索引
self.activity = activity
}

要完成这个任务,我们先要创建一个 NSUserActivity对象,用于保存当前页面的一些必要信息,以及要建立的索引的一些属性。

创建 NSUserActivity 对象时,需要传入一个初始化参数activityType,这个activityType类似于 Core Spotlight 中的域名标识,只是作用上有些区别。在 Spotlight 等入口,搜索结果被点击时,系统会根据这个标识,去区分由哪个应用来恢复这个活动。在进入应用后,我们也能够在回调中,利用这个属性来区分活动。

为了让系统知道我们的应用能处理哪些 activityType,我们也需要在 Info.plist 中,创建一个名为NSUserActivityTypes的 String 数组,标识出所有我们的应用能够处理的 activityType。

通过设置 NSUserActivitycontentAttributeSet属性,我们可以自定义索引在搜索结果中的样式。设置给contentAttributeSet属性的是一个CSSearchableItemAttributeSet实例,与 Core Spotlight 中的属性集相似,它包含大量的属性可以设置。

Note:Activity 在调用了 becomeCurrent() 方法激活后,必须对 Activity 进行强引用,防止它 dealloc 了,否则 Activity 很可能不会被加入索引。相关讨论见 Apple 论坛:Search APIs are working

2、实现回调

NSUserActivity 的回调入口与 Core Spotlight 的很相似。同样在AppDelegate中实现application:continueUserActivity:restorationHandler:方法,像上面说到的,我们可以通过 userActivityactivityType 属性来区分活动类型。此时userActivity的属性集也已经为空,只能访问到 userInfo 中的数据。

1
2
3
4
5
6
func application(UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: [AnyObject]? -> Void) -> Bool {
if userActivity.activityType == "com.meitu.punmy.user" {
// Restore app state for this userActivity and associated userInfo value.
}
return true
}

Web markup

Engage Web Content

由于本次分享的主要针对 Core Spotlight 和 NSActivity,Web markup 就只作简要介绍。

Web Markup允许应用将它们的内容映射到一个网站(如网页版美拍),从而在 Spotlight 或 Safari 中进行搜索,即使用户没有安装相关应用。用户点击相关结果后,如果没有安装应用,则通过 Safari 打开,如果已经安装应用,则跳转到对应页面。

苹果有类似于搜索引擎爬虫的机器人,能够抓取支持 Web markup 的网站来获取所需的信息(需要后台配置,并结合 Universal Link 使用)。抓取结果会储存在苹果的云索引服务器上,通过 Safari 和 Spotlight 提供给用户。

有关 Web markup 的详细内容可以访问官方文档:Mark Up Web Content

联合使用 Core Spotlight 和 NSUserActivity

苹果在官方文档中表示,三大搜索 API 是为了联合使用而设计的,混合使用多种 API 有助于提升搜索的覆盖率。但在实际使用中,混合使用多种 API 会有一些坑,下面大致讲一下一些注意事项。

###1、唯一标识的使用###
Core Spotlight 中的 uniqueIdentifier,NSUserActivity 中的 relatedUniqueIdentifier 属性,以及 Web markup 中的 webpageURL,都会被系统用于索引的关联。但系统不会平等地处理这几个东西。

例如,如果我们分别通过 Core Spotlight 和 NSActivity 生成了两个相同标识的索引/活动,那么在 Spotlight 中搜索到的只会是通过 Core Spotlight 设置的索引;而 NSActivity 生成的索引,则是用于 Siri Kit(如果你有使用 Siri Kit 的话)。

###2、索引的更新和删除###
通过 Core Spotlight 建立的索引,可以通过 CSSearchableIndex 实例的三个删除方法进行删除。也可以设置过期时间,由系统自动清除。默认的过期时间为一个月。
而通过 NSActivity 生成的索引,只能设置过期时间,由系统管理,无法手动删除。

要更新相同 API 建立的索引,只需要再建立一个相同 identifier 的索引或者活动,系统就会自动更新对应索引。但是,不同 API 生成的索引,即使 identifier 相同,也不会相互更新内容。

###3、搜索排序###
苹果对于搜索的排序主要依据以下几个维度:

  • 用户浏览 App 中内容的频率 (当我们调用 NSUserActivity 的becomeCurrent()方法时,系统会进行统计)
  • 用户对于应用中的内容的参与度(由“互动率”决定。“互动率”是基于两个数据计算的,分别是:用户点击与你应用相关的条目的次数,以及搜索结果中显示的应用相关的条目的数量)
  • 你的网站中某个网址的受欢迎程度,以及可用的结构化数据量。(Web markup)

坊间传闻,苹果为了防止世界被破坏,守护 iOS 的生态圈,在搜索结果的排序上,花费了重金进行优化。没有好好维护 iOS 生态圈的话,可能会导致应用的索引被降低排名,或是被踢出搜索结果。因此在使用搜索 API 时,我们需要注意:

  1. 防止过度索引;(不要把一大堆有的没的数据,都丢到系统索引中去)
  2. 尽快将用户带入内容页;(避免中间步骤,以及降低 App 启动时间)
  3. 如果创建的 NSUserActivity 与已有的 Core Spotlight 索引相同,那么就将它们用相同的 identifier 关联起来,这样每次激活 NSUserActivity 时,也能提升 Core Spotlight 索引的排名;

延迟进入内容页

END

使用 Search APIs 可以有效提升应用的用户体验,但要注意各种 API 的适用场景,同时维护好 iOS 的生态圈。

参考

官方文档:App Search Programming Guide