Remove NSUserActivity content from Spotlight?

I'm looking at adding Search support to one of my apps and I've content where it seems like

NSUserActivity
is the correct API to use. However, I'm not seeing anything handling the case when the user deletes the content from my app.


When you're using

NSUserActivity
, is there no way to remove stale content from the search index? Should I file a radar for this or would I be better off switching to the
CoreSpotlight
APIs for this type of access?

Replies

Looking in the header file there are three methods for deleting entries

- (void)deleteSearchableItemsWithIdentifiers:(NSArray<NSString *> *)identifiers completionHandler:(void (^ __nullable)(NSError * __nullable error))completionHandler;
- (void)deleteSearchableItemsWithDomainIdentifiers:(NSArray<NSString *> *)domainIdentifiers completionHandler:(void (^ __nullable)(NSError * __nullable error))completionHandler;
- (void)deleteAllSearchableItemsWithCompletionHandler:(void (^ __nullable)(NSError * __nullable error))completionHandler;


I could not get

deleteSearchableItemsWithIdentifiers: completionHandler:

to work but the more nuclear option

deleteAllSearchableItemsWithCompletionHandler:

and re-adding entries worked.

Lev - those methods relate to the

CSSearchableIndex
/
CSSearchableItem
interfaces, not the
NSUserActivity
interface. I guess I'm not shocked that
deleteAllSearchableItemsWithCompletionHandler
would work, but that seems more like a side effect that a guaranteed promise, at least given the current state of the documentation.


Maybe if you set the

relatedUniqueIdentifier
of an
NSUserActivity
's
contentSetAttribute
,
deleteSearchableItemsWithIdentifiers:completionHandler:
would work, but I don't think we have any information stating that one way or another. If that method only works when a
CSSearchableItem
entry explicitly exists in the index, though, it doesn't seem like we get much value out of using the
NSUserActivity
technique for transient / purgeable content.


(Now, if there is a guarantee that that deleting a

CSSearchableItem
from the index will also purge the associated
NSUserActivity
, then I can see the value is using the search index to build up your search index and presumably letting your user's actually workflow impact the search weighting of the content, but that's just speculation on my part.)

Can you give a little more information about what you're trying to do? NSUserActivity is intended to be used primarily with exactly what it says, user activity. Because you mentioned deleting content, it sounds like CoreSpotlight might be the more appropriate mechanism for this since that's more in line with indexing of user-generated content. You likely will use both NSUA and CS in conjunction with each other so it might just be a matter of how you're constructing these objects. In either case, having more context about what your content is and how your app works would be helpful.

Ah, nuts, got pulled away from my iOS 9 work and missed your response. Still trying to come up with the best way to track the new dev forums.


I'm working on a social sharing app that lets you have relationships with other people (shocking, I know) and I wanted to make various relationships searchable - enter somebody's name into Spotlight and have that result take you directly to the detail view in the app that displays your relationship with that person. If you choose to sever the relationship with that person, I want to remove them from the search index. My original implementation was creating the NSUA when the user navigated to a distinct relationship view.


My theory was you'd be more interested in seeing you old college friend Jane, who you interract with regularly, than Jane, your second cousin twice removed who you don't really like but can't de-friend because, well, family. I realize I could be attempting to recreate the smarts that you folks have already built into Spotlight.


I thought I saw Address Book in iOS 9b1 exhibiting the behavior that it only displayed uses in search results after viewing the user details, so I modeled my initial population of the search index after that - create an NSUA when actually interacting with the user. I'm now seeing all my contacts show up in search results regardless of whether I've viewed the detail view, so either something changed in b2 or I was misinterpreting what I was seeing before. (Would not at all surprise me if it was the latter.) I also didn't consider the possibility that Address Book might be using CS in the case of viewing contact details, if I was actually seeing the former.


I actually started the process of rewriting the code to use both CS and NSUA and linking them via the relatedUniqueIdentifier attribute (create the CS via data from our API and the NSUA entry when the user browses to the relationship view). I haven't had a chance to finish it yet and see if deleing the CS entry will also remove the results generated via NSUA.


...and, yes, I'm kicking myself for not figuring out these questions during WWDC and hitting the lab.

I'm working on a social sharing app that lets you have relationships with other people (shocking, I know) and I wanted to make various relationships searchable - enter somebody's name into Spotlight and have that result take you directly to the detail view in the app that displays your relationship with that person. If you choose to sever the relationship with that person, I want to remove them from the search index. My original implementation was creating the NSUA when the user navigated to a distinct relationship view.

I think what you should do is use CoreSpotlight to index the relationship/person information, and then use NSUA to layer on top of that the additional information about how the user navigates those relationships. A couple points to consider:

  • Using CoreSpotlight will allow you to index all the relationships a user has established regardless of whether they have viewed them in your app. If you didn't do this, users wouldn't be able to search for somebody without first having viewed them in the app. That's not the behavior I would expect.
  • You can use the same identifier for each of these APIs (e.g. using the relatedUniqueIdentifier property on NSUA) which lets the user's action in the app get tied to the CoreSpotlight items. This helps give more meaning to the CoreSpotlight items which can help make sure more frequently viewed relationships get ranked more highly.
  • Using CoreSpotlight you can update and delete these items over time as relationships end.

My theory was you'd be more interested in seeing you old college friend Jane, who you interract with regularly, than Jane, your second cousin twice removed who you don't really like but can't de-friend because, well, family. I realize I could be attempting to recreate the smarts that you folks have already built into Spotlight.

Right, this is what using CS + NSUA would give you. Since you'd be linking the two approaches through a unique ID, if you put all the "content" in the CoreSpotlight index, when you delete those items if the relationship ends then you should get the behavior you're looking for. The NSUserActivity would only be expressing the actions the user took in your app, the CoreSpotlight index is where the content would actually live.

I actually started the process of rewriting the code to use both CS and NSUA and linking them via the relatedUniqueIdentifier attribute (create the CS via data from our API and the NSUA entry when the user browses to the relationship view). I haven't had a chance to finish it yet and see if deleing the CS entry will also remove the results generated via NSUA.

Please post here with how things work out for you. And of course write any bug reports if you're not getting the results you expect, and post those numbers.

...and, yes, I'm kicking myself for not figuring out these questions during WWDC and hitting the lab.

Aw, don't do that! Many of the same people that are available in the WWDC labs are available here on the forums so you can get the same information here. 🙂

pdm: Thank you for your explanation. This helps me to wrap my head around the expectations as well. I want to make sure that I am thinking about the usage patterns as intended.


Let's assume I have a simple Notes app with two screens--a list view (ListVC) and a single note view (DetailVC). Let's say that the note has a Title and Body properties. I am assuming exlusive usage of NSUserActivity and no Core Spotlight integration.


Scenario 1:

* The user is looking at the list view

* The user taps on note and the DetailVC is pushed on to the nav stack with the activeNote set

* The DetailVC sets its userActivity with information appropriate for the activeNote

* The user leaves the app, searches in spotlight for the note ... and it appears ... great!


Scenario 2:

* The user goes back into the app, selects the same note and changes the title of the note

* The DetailVC updates the userActivity to reflect the new title

* The user leaves the app, searches in spotlight for the note ... and the same note appears twice under both the old and new title ... not so great.


Scenario 3:

* The user goes back into the app, and from the ListVC, decides to delete the note

* The user leaves the app, searches in spotlight for the note ... and the deleted note appears twice with both the old and new title ... really not so great.



This scenario, which feels rather typical to me ... leads me to the following conclusions, which I am hopeful that you can confirm:


1. If my app has user modifiable content, exclusive usage of NSUserActivity, without Core Spotlight is a bad idea.

As evidenced in the scenarios above, exclusive usage of NSUserActivity led spotlight to return inaccurate results in both an "Update" and "Delete" scenario.


"Update" failed because there is no unique identifer on an NSUserActivity. (From my tests, it appears that it is actually using the title property as the unique identifier, but it isn't allowing any updates to the item. The Spotlight search results are stuck with the original version of my note.)


"Delete" failed because there is no API to remove an NSUserActivity from the index, which of course would be dependent on a unique identifier.


(I thought that perhaps leveraging the relatedUniqueIdentifier might help, but as is documented, using this alone, without Core Spotlight leads to your item not being indexed.)



2. Exlcusive usage of NSUserActivity, only makes sense in an app with read-only, static content.

Given the scenarios above, I cannot think of any other way that using NSUserActivity exclusively makes sense with respect to Search. (Assuming the scenarios above concern you of course.)



3. When using Core Spotlight and NSUserActivity together, the item must be indexed by Core Spotlight before it can be indexed by NSUserActivity.

This is due to the issue around the relatedUniqueIdentifier only working if the item already exists in Core Spotlight.



Based on potential timing of indexing things in Core Spotlight, if using batching or working on a background thread, or perhaps only indexing the "top items" in Core Spotlight and not all 100k that might exist in your app, this would lead me to the following conclusion about a best practice ... which concerns me.

4. When using NSUserActivity and Core Spotlight, immediately before creating the NSUserActivity, synchronously add an entry to Core Spotlight.


For obvious reasons, this last assertion concerns me, as it sounds a bit like a hack, yet, it seems like the only way to predictably ensure that your NSUserActivity will make it into the search index.



Thank you for taking the time to consider these assertions. As mentioned, I am really trying to wrap my head around the "best practices" and the "intentions" when working with these APIs. Any and all pointers are greatly appreciated.


--Chris

Let's assume I have a simple Notes app with two screens--a list view (ListVC) and a single note view (DetailVC). Let's say that the note has a Title and Body properties.


Great example.


I am assuming exlusive usage of NSUserActivity and no Core Spotlight integration.


But we're off on the wrong foot. This is not the way I would recommend doing this. CoreSpotlight is designed to index user content, which your notes content would be considered. So you should be using CoreSpotlight here.


1. If my app has user modifiable content, exclusive usage of NSUserActivity, without Core Spotlight is a bad idea.


Correct.


2. Exlcusive usage of NSUserActivity, only makes sense in an app with read-only, static content.


Not necessarily. Your app might present content that is static, but that the user might want to be able to search without having interacted with it. That is, let's say your content presented shared notes which are read-only (e.g. from a shared group of coworkers). Your app should be able to index those shared notes that the user may never have even seen before. NSUserActivity is intended to be used as the user interacts with the content. That is, it is based on actual user activity. If you were to solely use NSUserActivity then the user would have to manually view each of the shared notes in order for them to be indexed. That's not right.


3. When using Core Spotlight and NSUserActivity together, the item must be indexed by Core Spotlight before it can be indexed by NSUserActivity.


Correct.


4. When using NSUserActivity and Core Spotlight, immediately before creating the NSUserActivity, synchronously add an entry to Core Spotlight.


That's fine. Given that you have to satisfy #3 above, this might be your only choice, and it's fine. The actual work of doing the indexing is not done synchronously so it shoudln't block the calling thread by any significant amount. Otherwise, in the example of a Notes app, you might kick off a background update of the index to add any new or unindexed content to the CoreSpotlight index, although you'll still potentially run into cases where the user interacts with a note before it's indexed in the background so you'll have to handle that case regardless. You might want to store some kind of index identifier with your notes to be able to identify this situation, and if you hit it then you can add it to CS synchronously as you've described. This would also give you a handy spot to store an identifier that you could use when the user deletes a note and you need to remove it from the CS index.


As for other resources, I'd encourage you to look at http://developer.apple.com/ios/search as we've updated various documents over the past few seeds. If you haven't already reviewed them it'd be a good idea to do so. Feel free to file bug reports if there is information that's not documented sufficiently.

pdm:


Thank you so much for your thoughtful responses. I truly appreciate it.


Over the past few days, I have read and reread all of the docs and watched the corresponding videos, so, in isolation, I generally understand the "how", but if anything, it just seems that the specifics of the higher level architecture in an actual app are vague or missing from any of the discussions in the docs. The questions I was asking were all in an attempt to try to reconcile and consolidate all of the individual, isolated pieces of knowledge.


All of your responses make complete sense and are appreciated.


I do have one follow up, as I believe I worded the assertion in a confusing manner.


With regards to:


2. Exclusive usage of NSUserActivity, only makes sense in an app with read-only, static content.


Your response makes complete sense; what I was trying to get at is perhaps better asked this way: Is there a use case when EXCLUSIVELY using NSUserActivity makes sense? The example I provided with static, read-only data is the only "possible" path I could come up with which would not give potentially incorrect search results, even though it would be non-ideal as being EXCLUSIVE, as you pointed out.



I also appreciate the suggestion of storing the CS identifier with the item, which could be used to identify if the item had been indexed yet. This is particularly interesting and appropriate when there might not be a unique identifier on your objects yet because they aren't necessary for your domain. (For example, there is no need for the title of my notes to be unique, so I wouldn't naturally have a unique identifier in my app. Using the CS auto-created identifier is a great suggestion.)



Again, thank you for your time. It's always tough learning something new. I just want to get inside of the head of the developers that built this to understand what their intentions were on the usage scenarios!


Thanks!


--Chris

In the area of app search, there are 3 different APIs involved: NSUserActivity, CoreSpotlight and Web Markup. I could envision a use case where there is data available to search through Web Markup (i.e. you've got a publicly accessible website that Applebot has scraped) and there's no local content to put into CoreSpotlight. In that case I could imagine using only NSUserActivity and creating links to the web markup results using the webpageURL.


Honestly, though, in all the discussions I've been having with developers about app search there has always been some use for CoreSpotlight. There's almost always some form of user-generated or user-relevant content that an app operates on that isn't based on user activity.


Hope that helps. This is a very large topic that can take a while to get your head around all the moving pieces and how they apply to your particular case (and rarely are any two cases exactly the same). Thanks for participating in the forum discussions!

pdm:


Thanks! This is great insight.


I truly appreciate your time and patience.


This has helped tremendously!


--Chris

Hi,


Many thanks for all the input


I have a question regarding combing CoreSpotlight with NSUserActivity.


I have dne the following

- Indexed what I want using CoreSpotlight (appears in search as expected)

- When user interacts with item, I create a NSUserActivity (with relatedUniqueIdentifier) no duplicates so I assume it works.


Now when I search and find the item I bring them back to where I want them to be but when I print my userInfo information its the following:

[kCSSearchableItemActivityIdentifier: AOA]


Which would have been ok if it was just a CoreSpotlight item, but I added a NSUserActivity to it. I was expecting since I added the NSUserActivity to it, that I would be able to get the activity.userInfo = ["symbol": title, "searchDescription": contentDescription]

I tried the NSUserActivity code by itself (without CoreSpotlight and relatedUniqueIdentifier) to ensure I can index it as just an NSUserActivity item and it work.


Also the item is categorized having the type userActivity.activityType == CSSearchableItemActionType


@available(iOS 9.0, *)
func createNSUserActivity(title: String, contentDescription: String, image: NSData?, uniqueIdentifier: String, domainIdentifier: String) {

    let attributeSet:CSSearchableItemAttributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeImage as String)
    attributeSet.contentDescription = contentDescription
/
    attributeSet.relatedUniqueIdentifier = title

    let activity: NSUserActivity = NSUserActivity(activityType: domainIdentifier)
    activity.title = title
    activity.keywords = NSSet(array: [title, contentDescription, "Stocks", "Markets"]) as! Set<String>
    activity.userInfo = ["symbol": title, "searchDescription": contentDescription]
    activity.contentAttributeSet = attributeSet

    activity.eligibleForSearch = true
    activity.eligibleForPublicIndexing = true
    activity.requiredUserInfoKeys = NSSet(array: ["title", "userInfo", "contentAttributeSet", "eligibleForSearch", "eligibleForPublicIndexing"]) as! Set<String>

    userActivity = activity
    activity.becomeCurrent()

    print("NSUserActivity created")
}
@available(iOS 9.0, *)
func addToSpotlight(title: String, contentDescription: String, image: NSData?, uniqueIdentifier: String, domainIdentifier: String) {

    let attributeSet:CSSearchableItemAttributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeImage as String)
    attributeSet.title = title
    attributeSet.contentDescription = contentDescription
    attributeSet.thumbnailData = image
    attributeSet.keywords = [title, contentDescription, "Stocks", "Markets"]

    let searchableItem = CSSearchableItem(uniqueIdentifier: uniqueIdentifier, domainIdentifier: domainIdentifier, attributeSet: attributeSet)

    CSSearchableIndex.defaultSearchableIndex().indexSearchableItems([searchableItem]) { (error) -> Void in
   
        if let error = error {
            print("Deindexing error: \(error.localizedDescription)")
        } else {
            print("Search item successfully indexed!")
        }
    }

}