Optimising CoreData fulltext query - objective-c

I have a large collection of CoreData objects which represent files. Periodically, I need to search this collection and find files which exist at a given path. Currently, I'm constructing the following NSPredicate and performing a fairly basic executeFetchRequest to find all results which match my query.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"path ==[c] %#", receivedPath];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Files" inManagedObjectContext:self.moc];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
[request setPredicate:predicate];
[request setFetchBatchSize:10];
NSError *error = nil;
NSArray *results = [self.moc executeFetchRequest:request error:&error];
[request release];
The issue I'm having is that this function is called fairly frequently and the executeFetchRequest is accounting for a significant percentage of execution time (Instruments tells me the function accounts for around 49.2% in total, with 98.1% of that total spent on the executeFetchRequest call). I'm using NSSQLiteStoreType for my persistent store and have the path attribute indexed in my MOM.
My question is, how can I optimise this? I've considered setting up a lowercasePath attribute, and ditching the [c] modifier on the comparison, but am unsure as to what impact (if any) that would have on execution time.
Any help would be greatly appreciated.

Removing the [c] should make a difference. ==[c] is probably translated to an SQLite LIKE operator for which SQLite cannot use indexes, if I remember correctly. Speaking of which, do you have the attribute marked as indexed in your model? If not, you should do so, but again, with a case-insensitive search, this probably doesn't make a difference.

Related

Standard for find or create object in Core Data?

Is there any standard or preferred way of implementing a "find or create object" in Core Data?
I was planning to use a class method in the NSManagedObject subclass, but want to make sure I'm getting the best performance.
For instance, does it make sense to cache fetches to avoid hitting the disk to much or is it a waste of time?
Class methods are fine. You will have to hit the disk to retrieve, and if not successful again to insert. I do not see a performance hit.
If you do this 1000s of times, of course it is more efficient to do it in memory. Fetch all relevant data, or even just the relevant field (with NSDictionaryResultType) and filter in memory with predicates. If you use KVC (key-value coding) this can be very efficient and elegant - I have used something like the following with 100.000s of records:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:"Entity"];
NSArray *result = [context executeFetchRequest:request error:nil];
NSArray *allIDs = [result valueForKeyPath:#"idAttribute"];
NSArray *existingIDs = [allIDs filteredArrayUsingPredicate:
[NSPredicate predicateWithFormat:#"self in %#", idsToCheck]];
NSArray *newIDs = [idsToCheck filteredArrayUsingPredicate:
[NSPredicate predicateWithFormat:#"self not in %#" existingIDs];

Fetching data using Core Data

I have searched quite a lot on the internet but can't find what I'm looking for.
I have this model where it could be a lot of users. So I have an entity called User. The user has an NSSet of records. And I want to fetch records from given user. I'm trying to do it like this but it still returns records from all users.
NSManagedObjectContext *context = _backgroundContext;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Record"
inManagedObjectContext:context];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(date >= %#)",date];
NSPredicate *predicate1 = [NSPredicate predicateWithFormat:#"User = %#",currentUser];
NSPredicate *predicates = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:predicate,predicate1, nil]];
[fetchRequest setPredicate:predicates];
[fetchRequest setPredicate:predicate];
[fetchRequest setEntity:entity];
NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
I know it shouldn't be hard, but I can't find what it is, and I'm hoping some of you could help. Thanks in advance!
EDIT:
As I said I have an entity User which has:
NSString name,
int age,
NSSet records, ...
Record has its own properties such as:
NSDate date,
NSString name,
NSString event,
...
I want to form a fetch request to get records just from specific user. And I don't know how to do it, because I'm getting all of the records from every user.
records has a To-Many relationship. I can get records like currentUser.records, but i can't get user using record.User.
I'm assuming your model looks roughly like this, i.e. the records relationship has an inverse relationship called user:
It's importantant that this relationship is an inverse relationship because otherwise Core Data will not automatically maintain it. You can then query all Records for the given User on/after the given date like this:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Record"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"user = %# AND (date >= %#)", currentUser, date];
NSError *error = nil;
NSArray *records = [_backgroundContext executeFetchRequest:fetchRequest error:&error];
NSLog(#"records: %#", records);
I've rewritten your code to be more compact, but the important change is that this will create the query in just one go.
The error is here:
[fetchRequest setPredicate:predicates];
[fetchRequest setPredicate:predicate];
After setting the compound predicate, you overwrite it with the predicate for date alone. You probably want to delete the second line.
EDIT: The fetch request requires that you have defined a inverse relationship user from Record to User and use the exact name of this relationship in the predicate.
An alternative solution is to use the "forward" relationship from User to Record and filter the result:
NSSet *records = [currentUser.records filteredSetUsingPredicate:predicates];
or, if you prefer an array
NSArray *records = [[currentUser.records allObjects] filteredArrayUsingPredicate:predicates];
As Martin R said, you're replacing your predicate.
As he also mention, the user property should be lowercase, as I'm pretty sure that core-data enforces this. It should give you an error if you try creating a relationship with an upper case letter, so try
NSPredicate *predicate1 = [NSPredicate predicateWithFormat:#"user = %#",currentUser];
instead. Obviously if your model doesn't define a property for user then that won't work either and you'll need to change it to whatever your model actually has, but that should work otherwise.
Edit:
Try adding an inverse relationship in your model. So your structure would look like :
user
NSString name,
int age,
NSSet records, ...
Record
NSDate date,
NSString name,
NSString event,
User user,
...
Then every time you create a new record, as well as adding the record to the users record set, set the user on the record object as well. This will make fetching the records a lot easier, and also it allows core data to keep data integrity:
"You should typically model relationships in both directions, and specify the inverse
relationships appropriately. Core Data uses this information to ensure the consistency of the
object graph if a change is made"
(Core Data programming guide)
Once this is setup, you should easily be able to get the records user by simply calling record.user
If you need multiple users for multiple records, then simply setup the relationship as a to-many relationship, and change the User user property to NSSet user.
If you don't want to change your models this much, then I'm a bit confused with your question. You say you want to get the records for a given user? If so, then why don't you simply call user.records as you have that relationship already defined? There's no need for an entire fetch request for that, let core data manage that one for you.
If you then want to filter the resulting Records based on the date, you can apply a predicate to the resulting array yourself, again without the need for a fetch request.
I have found what I've searched! I just needed to form a predicate using this
NSPredicate *predicate1 = [NSPredicate predicateWithFormat:#"SELF = %#",delegate.currentUser];
EDIT
Well it seems that i was wrong :/ That only seemed to work.

Complex NSPredicate traversing multiple Entities and relationship types

I'm having a tough time solving this predicate issue since the database structure is a bit complex. I have the following database structure, or at least what is of concern for this question:
PUBLISHER<< --- >>PUBLICIST<<--BOOK<<--->AUTHOR<<-->>AGENTS
I tried the following predicate that I have used when I traversed relationships in the past, but not to this degree. I should mention that I have an NSArray with agent names that I want to query against the database to determine a list of publishing houses the agent works with:
NSArray *agentNames = [NSArray arrayWithObjects:Dan, Hunter, Sloan, Jackson];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Publisher" inManagedObjectContext:context];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor
sortDescriptorWithKey:#"publisherName" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)] ];
request.predicate = [NSPredicate predicateWithFormat:#"ANY pubHouse.publicist.assignedBook.authorRep.agentName IN %#", [agentNames valueForKey:#"agentName"]];
request.fetchBatchSize = 20;
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
When I run the previous predicate I get the following warning:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'unimplemented SQL generation for predicate : ANY pubHouse.publicist.assignedBook.authorRep.agentName IN
I believe the predicate breaks when I travel the Book Entity relationship to the Author Entity and then onto the Agent Entity. At this point suggestions would help. Thanks
Your problem is that the predicate parser has no clue what set ANY should be applied to.
With this data model:
PUBLISHER<< --- >>PUBLICIST<<-->BOOK<<--->AUTHOR<<-->>AGENTS
… your keypath:
pubHouse.publicist.assignedBook.authorRep.agentName
… in terms of objects and set of the relationship looks something like:
object.set.object.object.set
So, that is two sets that the ANY could apply to.
You could try to build a subquery to handle the predicate but if you have to transverse that many relationships, your fetch will involve a big chunk of your data and will be very, very slow (assuming you get it work in the first place.)
Usually when you end up with a convoluted predicate like this it indicates that you are approaching the problem from the wrong end. In this case, it would be easier to start with a simple predicate like:
NSArray *agentNames = [NSArray arrayWithObjects:Dan, Hunter, Sloan, Jackson];
request.entity = [NSEntityDescription entityForName:#"Agent" inManagedObjectContext:context];
request.predicate = [NSPredicate predicateWithFormat:#"agentName IN %#", agentNames];
Then you would walk the relationship keypath of:
authors.books.publicist.publishers
… to find all the related publishers.
I think that you will have trouble no matter what you do because having more than one to-many-to-many relationship e.g.
PUBLISHER<<--->>PUBLICIST
… increases the complexity of predicates and relationship walks exponentially. Usually, in such a case, you may need an additional entity to more thoroughly model one of the relationships. That usually reduces the complexity of the data model itself which simplifies fetches and walks.
Perhaps NSArray has a problem with valueForKey:. I have seen a solution that uses NSCompoundPredicate adding the array items in a loop.
BTW, in your chain of multiple relationships, aren't you missing your authors?
Just wrap the IN clause of your predicate string in parentheses:
#"ANY (pubHouse.publicist.assignedBook.authorRep.agentName IN %#)"
Would be glad to know if it works.

Core Data NSFetchRequest for Specific Relationship?

I'm transitioning an existing data model that was previously stored in XML to Core Data, so I'm trying to learn the ropes as properly as possible. Core Data is obviously one of those technologies that isn't going anywhere anytime soon, so I might as well "learn it right."
Take for example a Core Data model with two Entities:
Person
Food
Person has 2 one-to-many relationships with Food:
favoriteFoods (1-to-many)
hatedFoods (1-to-many)
(Both Person and Food are subclasses of NSManagedObject as well.)
In the previous data model, Person maintained two NSArray instance variables. If I wanted favorite foods, I could call:
Person *fred = [[Person alloc] init];
NSArray *fredsFavorites = fred.favoriteFoods;
Easy squeezy.
I'm reading through the documentation for Core Data, and I can't seem to find the right way to obtain this NSArray given an NSFetchRequest, because I can't define which relationship I want to obtain objects from.
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Food" inManagedObjectContext:[fred managedObjectContext]]];
[request setIncludesSubentities:NO];
NSArray *fredsFavoriteAndHatedFoods = [[fred managedObjectContext] executeFetchRequest:request error:nil];
This returns all of the Food items stored in both favoriteFoods and hatedFoods. How can I split these up? Surely there's a simple explanation, but I don't currently grasp the concept well enough to explain it in Core Data jargon, thus my Google searches are fruitless.
The most straightforward way to get it is to simply access the relationship NSSet directly:
NSArray *fredsFavorites = [fred.favoriteFoods allObjects];
(I've shown how to get an NSArray from the resulting NSSet).
Alternatively, if you haven inverse relationship set up (which you should) you could use a fetch request like this:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Food" inManagedObjectContext:[fred managedObjectContext]]];
[request setPredicate:[NSPredicate predicateWithFormat:#"ANY personsWithThisAsFavorite == %#", fred]];
NSArray *fredsFavoriteFoods = [[fred managedObjectContext] executeFetchRequest:request error:nil];
This assumes that personsWithThisAsFavorite is the inverse relationship to favoriteFoods. Unless I'm reading your example wrong, favoriteFoods should really be a many-to-many relationship, since a person can have multiple favorite foods, and a food can have multiple people with that food as a favorite.
(Note that I haven't tested this, so the NSPredicate might not be 100% correct)

iPhone's Core Data crashes on fetch request

I'm using the following code to grab a few objects from SQLite store (which is a prepared SQLite db file, generated with Core Data on desktop):
NSFetchRequest * request = [[NSFetchRequest alloc] init];
[request setEntity: wordEntityDescription];
[request setPredicate: [NSPredicate predicateWithFormat: #"word = %#", searchText]];
NSError * error = [[NSError alloc] init];
NSArray * results = [[dao managedObjectContext] executeFetchRequest: request error: &error];
Eveyrthing seems to be setup properly, but executeFetchRequest:error: fails deeply inside Core Data (on NSSQLCore _newRowsForFetchPlan:selectedBy:withArgument) producing 256 error to the outside code.
The only kink I had setting up managedObjectContext is I had to specify NSIgnorePersistentStoreVersioningOption option to addPersistentStoreWithType as it was constantly producing 134100 error (and yes, I'm sure my models are just identical: I re-used the model from the project that produced the SQL file).
Any ideas?
P.S. Don't mind code style, it's just a scratch pad. And, of course, feel free to request any additional info. It would be really great if someone could help.
Update 1
Alex Reynolds, thanks for willingness to help :)
The code (hope that's what you wanted to see):
NSEntityDescription * wordEntityDescription; //that's the declaration (Captain Obviousity :)
wordEntityDescription = [NSEntityDescription entityForName: #"Word" inManagedObjectContext: ctx];
As for predicate – never mind. I was removing the predicate at all (to just grab all records) and this didn't make any differences.
Again, the same code works just fine in the desktop application, and that drives me crazy (of course, I would need to add some memory management stuff, but it at least should produce nearly the same behavior, shouldn't it?)
Can you add code to show how wordEntityDescription is defined?
Also, I think you want:
NSError *error = nil;
You may want to switch the equals symbol to like and use tick marks around the searchText field:
[request setPredicate: [NSPredicate predicateWithFormat: #"word like '%#'", searchText]];
NSPredicate objects are not put together like SQL, unfortunately. Check out Apple's NSPredicate programming guide for more info.