iOS 10 Spotlight App Discovery: NSUserActivity and Search Relevancy

When I heard that iOS 10 was the first version to support app content discovery, I was overjoyed. The dream of app discovery through Apple’s native search would finally be a reality.

But reading through the documentation, I found myself rather confused on how to get everything set up. After diligent reading and extensive testing, I feel that I have a good grasp of the fundamentals on how all of these pieces fit together. I’ll be doing a series of posts around the subject of Spotlight search.

At Branch, we’ve built a number of SDK methods that simplify most of this indexing process so that you don’t have to worry about it. Once you get the service all integrated, head to our Spotlight docs to ensure you’re set up properly. Plus, if you use the Branch SDK to index your content, you benefit from two immediate advantages:

  1. You can measure the number of users clicking through Spotlight search
  2. You can use your existing Branch deep link routing to load the right page after a user clicks from Spotlight.
Spotlight Local Index

The Spotlight local index is entirely private to the device and managed completely separately from the cloud index. Content put in the local index is only accessible to the user of that specific device, and not to anyone else. Think of it as a private search database for a single user.

Here’s an example from the Branch iOS Testbed app. You’ll notice the results are listed in a “Top Hits” section once the user starts to query Spotlight:

Content SearchlightSpotlight Search APIs for Local Search Index

For definition, I’m referring to the collection of CSSearchableIndex, CSSearchableItem, and CSSearchableItemAttributeSet as the Spotlight Search APIs, since they’re a part of the CoreSpotlight.framework that must be included to use them. These APIs are specifically to be used for managing the local search index and do not contribute to the cloud index. It’s an extremely powerful API that allows your app content to be indexed in the local search.

In terms of intended use, Apple has informed me that developers should use these APIs to manage a list of things put in the local index asynchronously from user behavior. The API is specifically indexing a large quantity of objects all at once. (Note: this is quite different than the intended use case for NSUserActivity, which I’ll describe in the section below.)

For example, if you have a contacts app, you can put the entire list of contacts into the local index on app startup, then delete and update the index as the user keeps their contact list up to date. Then, the user could query their contact list through the Spotlight interface and be deep linked back to your app after clicking a result.

Spotlight Cloud Index

The Spotlight cloud index is a completely separate search index of objects that Apple maintains to service generic web and app search. Anything that makes it into the cloud index will be discoverable across all devices through Spotlight search. This is a new feature that Apple is focusing on for iOS 10, and they are extremely conscious about ensuring that no user privacy is compromised through this functionality.

In order to index something here, it must be scraped by the new “Applebot.” Here’s an example user agent for the Applebot:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/600.2.5 (KHTML, like Gecko) Version/8.0.2 Safari/600.2.5 (Applebot/0.1; +https://www.apple.com/go/applebot)

This, of course, implies that for something to appear in the cloud index, it must have a web URL where content could be scraped. From Apple’s perspective, the fact that it contains a web URL that is scrapable by their robot means that it’s publicly available information that won’t violate user privacy.

In order to populate the relevant title, description, image, and keywords for a given object in the cloud index, Apple has suggested that they will be using the Open Graph protocol to populate this metadata. You must have these tags correctly filled in for a given web URL if you want your content to appear in this index.

NSUserActivity

The NSUserActivity class is where most of the confusion is introduced when it comes to indexing your app content. The confusion originates from the fact that NSUserActivity can be used to populate both the local index and cloud index.

First, let me describe Apple’s intention with this class. The NSUserActivity class is meant to index only the objects that users actually view. This is why Apple added the restriction that an NSUserActivity must be associated to a UIViewController or one of its subclasses. If you try to index objects asynchronously (like the contacts example above) with NSUserActivity, you’ll find that it won’t work. Think of this method as a “browsing history” index.

However, when you populate the fields of an NSUserActivity, assign it to a UIViewController and call becomeCurrenton the instance, it’ll add the object to the local index and if the item is marked searchable and public, it is eligible for the cloud index via Differential Privacy (more later). In the above section on the cloud index, you’ll notice Apple will only include items in the cloud index that have a web URL, and only scrape the metadata for that object with Applebot. This is important. Since Apple uses differential privacy, the NSUserActivity metadata (title, image) and the CSSearchableItemAttributeSet (title, description, image) that you assign to the NSUserActivity class instance never leaves the device; and Apple relies on scraping the webUrl discovered with Differential privacy.

Note: To help explain differential Privacy, you might want to link to the Privacy keynote from WWDC. We’re also writing a much more in-depth piece on the subject. 

Apple also relayed how absolutely critical it is that you set the relatedUniqueIdentifier(property of CSSearchableItemAttributeSet which is defined and assigned to the NSUseractivity) consistently across all objects viewed, across the different instances of your app. This property is used by the team for de-duping across many instances of the content. And they recommend that you set the unique identifier to be the same value as the NSUserActivity webUrl.

If this is making your eyes glaze over, I’d recommend just dropping in the Branch SDK, creating Branch Universal Objects and marking theautomaticallyListOnSpotlightboolean to true. We’ll make sure the NSUserActivity is initialized correctly and listed on local and cloud Spotlight. See the docs for more details.

Differential Privacy

One of the biggest challenges Apple has faced when it comes to providing a compelling user experience in search is the high standard of user privacy they want to maintain. Since Apple controls the iOS platform, and they’ve built the NSUserActivity and Spotlight APIs, you would imagine they could immediately incorporate this data for search indexing and ranking. However, this would mean they are monitoring individual user interactions with these APIs and tracking this behavior and was shot down by the internal privacy team.

Enter “differential privacy.” If user interactions only are used after enough iOS users interact with a piece of content via the aforementioned indexing APIs, it’d be impossible to ever point to a single user as having interacted with that content. Here’s a more technical walkthrough of how it would be implemented:

  1. Keep a record of all objects indexed
  2. Only track the count of times each object was interacted with
  3. Once the count goes above a defined threshold, the interaction data is eligible to be used in search ranking

What this does is diffuse the interaction data across the ‘threshold’ number of iOS users, so that no single user actually contributes their interaction data to the search relevancy and the data can never be tracked back to the user.  And once the threshold is reached, the interaction data serves to benefit the ranking cloud index for all iOS users, not just the ones that interacted with the content.

Relevancy in Cloud Index

There is one unique facet in particular that you as the developer can control for your app: engagement data from differential privacy above.

With iOS 10, NSUserActivity engagement data will actually be included to improve the relevancy of search. This will only happen once the threshold of engagement (See: differential privacy) is reached and the threshold is a continuously moving target, as calculated by statistical methods to guarantee the privacy of the user. However, if your NSUserActivity has the web URL correctly assigned, and your web URL is correctly hosting the Open Graph metadata for that content, you’ll be added to the cloud index and start registering engagement counts.

Once those engagement counts cross the differential privacy threshold, your results will start increasing in relevance for user queries. This means for you as the mobile-focused business, your app engagement data will finally play to your advantage when it comes to content discovery. This is the dream: discovery through app indexing and tracking. Yes, it does require you to have a website where all the content is stored, but it’s a great first step.

If you couldn’t tell by now, it’s absolutely critical that you are creating NSUserActivities with the correct metadata for every UIViewController. We’d recommend that you use the Branch SDK to create Branch Universal Objects, which will create the properly formatted and deduped NSUserActivities on your behalf, plus outfit them with correct Branch links to handle tracking and deep linking. See these docs for more info.

Relevancy in Local Index

Interaction with search results from the local index will be used to improve relevancy for both local and cloud search for an app. While there’s no quantified information about exactly how and when, we were given a snippet of explanation: when a user engages with a search result based on a query on the local index while your app will increase in relevance on the cloud index since result information does not leave the device, the result itself will increase in relevancy against that query. This does not track individual search patterns, but rather increases the relevance per query :: result pair locally and query :: app pair globally.

An example would be, let’s say that people search for “cookie recipe” and your result appears from the local index “Mama’s chocolate chip cookie recipe.” If a user clicks on that result, opening your app, the relevancy and ranking of “Mama’s chocolate chip cookie recipe” will improve in the local and the relevancy of your app would increase in the cloud search index for “cookie recipe”.

In order to tie the two indices together and benefit from the relevancy, you’d need to ensure that the relatedUniqueIdentifierin the CSSearchableItemAttributeSet is set the same across both NSUserActivity and your Core Spotlight APIs.

Get started with Spotlight indexing on both the cloud and local index today using Branch’s deep linking SDKs at no charge.