views:

208

answers:

5

My app has a UISearchBar allowing user to enter search keywords. Each keystroke executes a Core Data query in order to display the results as text in search bar changes.

The problem is that search bar keystrokes are lagging pretty bad... surely because of slow fetching. Any ideas how to improve the performance?

My Core Data is backed with sqlite data store which contains 1000 objects.

// searchKeyword is the string appears in UISearchBar
// Both title and author may contain several words so I can't use BEGINSWITH
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(author CONTAINS[c] %@) OR (title CONTAINS[c] %@)", searchKeyword, searchKeyword];

NSEntityDescription* entity = [NSEntityDescription entityForName:@"Book" inManagedObjectContext:managedObjectContext];

NSFetchRequest* request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
[request setPredicate:predicate];
[request setFetchLimit:10];

NSSortDescriptor* sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"title" ascending:YES];
NSArray* sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];

[sortDescriptor release];
[sortDescriptors release];

execute request and fetch the results
fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                managedObjectContext:managedObjectContext
                                sectionNameKeyPath:nil
                                cacheName:nil];
NSError* error = nil;
BOOL success = [fetchedResultsController performFetch:&error];
[request release];
A: 

You can create a trie incrementally in order to have an index for past queries (result sets are pointed by the leaf nodes). but it won't increase the performance of a single query. You can also tweak the keystroke system, don't do a fetch after each single keystroke but only after a keystroke after which a time interval (as a threshold) passed before another keystroke is recognized

rano
+2  A: 

While it won't speed up the query, putting the lookup into a background thread will stop the keystrokes from lagging.

cobbal
+1 But remember to execute only one background query at a time, and to get the latest search string on each query (or otherwise coalesce text field updates), and lower the thread/NSOperation priority until the UI performs decently.
tc.
Thanks for the tips. I am really trying to avoid multithread solution but I will do it if I'll have no other choice.
Joshua
A: 

When starting the app, build up a full trie, and adapt it when editing. Do not use that stupid predicate. You're only fetching a 1000 records, so that should take no time.

Stephan Eggermont
Sorry, I am not sure how can I build a full trie on app startup. Can I really build such title\author trie in advance?
Joshua
Just build it in - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
Stephan Eggermont
better not to build it entirely but incrementally as I suggested in my answer.
rano
SQLite might be slow, but it isn't so slow that you cannot read 1000 records of two fields when starting, isn't it? Otherwise I would seriously recommend using a real datastore.
Stephan Eggermont
Building incrementally when querying is no usable solution for an iPhone app. You *need* fast results the first time.
Stephan Eggermont
+2  A: 

Using CONTAINS slows it down. What you need to do is create a new table called searchWords (or whatever), and in that store all the words within your titles, made lower case and with accents removed. These have a relationship linking them back to the original objects. Make sure the word field is indexed.

Perform your query on this table, but instead of using CONTAINS or BEGINS, do something like

word > "term" AND word < "tern"

Note that the first string in there is the search term, and the second is the search term with the last character incremented. This allows Core Data to use the SQL index to perform the search.

Apple have a WWDC session that explains this, including sample code. I can't remember which it is off hand (not at my Mac), but it's one of the Core Data ones. The sample code contains a class that handles normalising the string (I.e. Removing case), and incrementing the last character of a word in a unicode aware way.

Amorya
This is a great solution! Thanks!!I will look for that session and post it here if I'll find it.
Joshua
A: 

While Amoyra's recommendation is sound, you also have a design issue.

Only the first letter typed into the search field should ever hit the disk. After the first letter you should only be refining the search results of what is already in memory.

You should filter the array that is in memory (from the first letter search) using the predicate you already have and avoid executing a fetch request as it is unnecessary.

In addition, if you are going to search on data that is already in memory (such as living in a NSFetchedResultsController) then your entire search should only hit the objects in memory. Going out to disk is unnecessary and terribly wasteful.

Marcus S. Zarra
Hi Marcus, this is really interesting point. But I think there is an issue with that solution. Clicking 'a' or 'e' as the first letter, will probably bring most of the objects from database (if not all). When dealing with large DB this single fetch can take very long time not talking about memory implications...
Joshua
That situation would not change no matter what implementation you use. Core Data will pull in skeletons so the memory usage would be as low as possible. As you filter that memory usage would go down. The solution definitely works as I have used it many times.
Marcus S. Zarra