Add support for hooking into the RKSearchIndexer via a delegate

* Supports replacing the RKSearchWord fetch strategy to enable caching for performance
* Supports declining creation of search words
* Supports declining of indexing for specific objects
* Supports notification when objects are indexed and search words are added to the index
This commit is contained in:
Blake Watters
2012-11-12 16:18:36 -05:00
parent 7d9087b722
commit 6837d0d1b9
5 changed files with 268 additions and 18 deletions

View File

@@ -42,8 +42,7 @@
@param entity The Core Data entity description for the managed objects being cached.
@param attributeName The name of an attribute within the cached entity that acts as the cache key.
@param context The managed object context the cache retrieves the cached
objects from
@param context The managed object context the cache retrieves the cached objects from.
@return The receiver, initialized with the given entity, attribute, and managed object
context.
*/

View File

@@ -20,11 +20,15 @@
#import <CoreData/CoreData.h>
@class RKSearchWord;
/**
The key for an NSArray object that indentifies the list of searchable attributes in the user info dictionary of an NSEntityDescription.
*/
extern NSString * const RKSearchableAttributeNamesUserInfoKey;
@protocol RKSearchIndexerDelegate;
/**
The `RKSearchIndexer` class provides support for adding full text searching to Core Data entities and managing the indexing of managed object instances of searchable entities.
*/
@@ -64,6 +68,8 @@ extern NSString * const RKSearchableAttributeNamesUserInfoKey;
*/
@property (nonatomic, strong) NSManagedObjectContext *indexingContext;
@property (nonatomic, weak) id<RKSearchIndexerDelegate> delegate;
///---------------------------------------------------
/// @name Indexing Changes in a Managed Object Context
///---------------------------------------------------
@@ -141,3 +147,74 @@ extern NSString * const RKSearchableAttributeNamesUserInfoKey;
- (void)waitUntilAllIndexingOperationsAreFinished;
@end
/**
Objects that acts as the delegate for a `RKSearchIndexer` object must adopt the `RKSearchIndexerDelegate` protocol. The delegate may customize the behavior of the search indexer to match the needs of the application in several ways. The delegate may provide an alternate implementation for fetching an existing `RKSearchWord` managed object for a given word, it is consulted when the indexer determines that a new search word object is to be inserted and may decline the insertion, and it is notified after a new search word has been inserted.
*/
@protocol RKSearchIndexerDelegate <NSObject>
@optional
///-------------------------------------
/// @name Fetching Existing Search Words
///-------------------------------------
/**
Asks the delegate for an existing search word object for a given word in the managed object context being indexed. If no search word is found for the given word, then `nil` is to be returned.
By default, the search indexer creates and executes a fetch request against the context being indexed for each word during indexing. For large data-sets, this can wind up taking a significant amount of time. By providing an implementation of `searchIndexer:searchWordForWord:inManagedObjectContext:error:`, the delegate can be used to implement a caching scheme to reduce the overhead associated with the execution of these fetch requests.
@param searchIndexer The search indexer object performing the indexing.
@param word The search word for which to retrieve an existing
@param managedObjectContext The managed object context in which indexing is taking place.
@param error A pointer to an error object to be set in the event an error occurs.
@return The `RKSearchWord` object corresponding to the given word, or `nil` if none could be found. In the event an error occurs, `nil` is to be returned and the value of the error property is to be set to a pointer to an `NSError` object describing the failure.
*/
- (RKSearchWord *)searchIndexer:(RKSearchIndexer *)searchIndexer searchWordForWord:(NSString *)word inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext error:(NSError **)error;
///-------------------------------------------
/// @name Tracking Indexing of Managed Objects
///-------------------------------------------
/**
Asks the delegate is a managed object should be indexed.
@param searchIndexer The search indexer object performing the indexing.
@param managedObject The managed object the indexer is preparing to index.
@return `YES` if indexing should proceed, else `NO`.
*/
- (BOOL)searchIndexer:(RKSearchIndexer *)searchIndexer shouldIndexManagedObject:(NSManagedObject *)managedObject;
/**
Tells the delegate that the indexer has finished indexing a managed object.
@param searchIndexer The search indexer object performing the indexing.
@param managedObject The managed object the indexer has just finished indexing.
*/
- (void)searchIndexer:(RKSearchIndexer *)searchIndxer didIndexManagedObject:(NSManagedObject *)managedObject;
///-----------------------------------------
/// @name Tracking Insertion of Search Words
///-----------------------------------------
/**
Asks the delegate if the indexer should insert a new search word for a word that does not currently exist in the index.
@param searchIndexer The search indexer object performing the indexing.
@param word A search word that appears in an indexed object but does not yet exist in the index.
@param managedObjectContext The managed object context in which indexing is taking place.
@return `YES` if the indexer should insert an `RKSearchWord` object for the given word, else `NO`.
*/
- (BOOL)searchIndexer:(RKSearchIndexer *)searchIndexer shouldInsertSearchWordForWord:(NSString *)word inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext;
/**
Tells the delegate that the indexer has inserted a new search word object for a word.
@param searchIndexer The search indexer object performing the indexing.
@param searchWord The search word that was inserted.
@param word The word for which the search word object was created.
@param managedObjectContext The managed object context in which indexing is taking place.
*/
- (void)searchIndexer:(RKSearchIndexer *)searchIndexer didInsertSearchWord:(RKSearchWord *)searchWord forWord:(NSString *)word inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext;
@end

View File

@@ -172,16 +172,30 @@ NSString * const RKSearchableAttributeNamesUserInfoKey = @"RestKitSearchableAttr
NSSet *tokens = [searchTokenizer tokenize:attributeValue];
for (NSString *word in tokens) {
if (word && [word length] > 0) {
fetchRequest.predicate = [predicateTemplate predicateWithSubstitutionVariables:@{ @"SEARCH_WORD" : word }];
RKSearchWord *searchWord = nil;
NSError *error = nil;
NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (results) {
RKSearchWord *searchWord;
if ([results count] == 0) {
if ([self.delegate respondsToSelector:@selector(searchIndexer:searchWordForWord:inManagedObjectContext:error:)]) {
// Let our delegate retrieve an existing search word
searchWord = [self.delegate searchIndexer:self searchWordForWord:word inManagedObjectContext:managedObjectContext error:&error];
} else {
// Fall back to vanilla fetch request
fetchRequest.predicate = [predicateTemplate predicateWithSubstitutionVariables:@{ @"SEARCH_WORD" : word }];
NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
searchWord = ([results count] > 0) ? [results objectAtIndex:0] : nil;
}
if (error == nil) {
if (! searchWord) {
if ([self.delegate respondsToSelector:@selector(searchIndexer:shouldInsertSearchWordForWord:inManagedObjectContext:)]) {
if (! [self.delegate searchIndexer:self shouldInsertSearchWordForWord:word inManagedObjectContext:managedObjectContext]) {
continue;
}
}
searchWord = [NSEntityDescription insertNewObjectForEntityForName:RKSearchWordEntityName inManagedObjectContext:managedObjectContext];
searchWord.word = word;
} else {
searchWord = [results objectAtIndex:0];
if ([self.delegate respondsToSelector:@selector(searchIndexer:didInsertSearchWord:forWord:inManagedObjectContext:)]) {
[self.delegate searchIndexer:self didInsertSearchWord:searchWord forWord:word inManagedObjectContext:managedObjectContext];
}
}
NSAssert([[searchWord managedObjectContext] isEqual:managedObjectContext], @"Serious Core Data error: Expected `NSManagedObject` for the 'RKSearchWord' entity in context %@, but got one in %@", managedObject, [searchWord managedObjectContext]);
@@ -204,6 +218,10 @@ NSString * const RKSearchableAttributeNamesUserInfoKey = @"RestKitSearchableAttr
[managedObject setValue:searchWords forKey:RKSearchWordsRelationshipName];
RKLogTrace(@"Indexed search words: %@", [searchWords valueForKey:RKSearchWordAttributeName]);
searchWordCount = [searchWords count];
if ([self.delegate respondsToSelector:@selector(searchIndexer:didIndexManagedObject:)]) {
[self.delegate searchIndexer:self didIndexManagedObject:managedObject];
}
}
}];
@@ -232,6 +250,9 @@ NSString * const RKSearchableAttributeNamesUserInfoKey = @"RestKitSearchableAttr
NSUInteger totalObjects = [objectsToIndex count];
__block NSMutableSet *indexedIDs = [NSMutableSet setWithCapacity:totalObjects];
for (NSManagedObject *managedObject in objectsToIndex) {
if ([self.delegate respondsToSelector:@selector(searchIndexer:shouldIndexManagedObject:)]) {
if (! [self.delegate searchIndexer:self shouldIndexManagedObject:managedObject]) continue;
}
[self indexManagedObject:managedObject withProgressBlock:^(NSManagedObject *managedObject, RKSearchWord *searchWord, BOOL *stop) {
if (totalObjects < 250) return;
if ([indexedIDs containsObject:[managedObject objectID]]) return;
@@ -245,6 +266,9 @@ NSString * const RKSearchableAttributeNamesUserInfoKey = @"RestKitSearchableAttr
} else {
// Perform asynchronous indexing
for (NSManagedObject *managedObject in objectsToIndex) {
if ([self.delegate respondsToSelector:@selector(searchIndexer:shouldIndexManagedObject:)]) {
if (! [self.delegate searchIndexer:self shouldIndexManagedObject:managedObject]) continue;
}
[self.operationQueue addOperationWithBlock:^{
[self indexManagedObject:managedObject];
}];
@@ -289,10 +313,16 @@ NSString * const RKSearchableAttributeNamesUserInfoKey = @"RestKitSearchableAttr
NSManagedObject *managedObject = [self.indexingContext existingObjectWithID:objectID error:&error];
NSAssert([[managedObject managedObjectContext] isEqual:self.indexingContext], @"Serious Core Data error: Asked for an `NSManagedObject` with ID in indexing context %@, but got one in %@", objectID, self.indexingContext, [managedObject managedObjectContext]);
if (managedObject && error == nil) {
[self indexManagedObject:managedObject withProgressBlock:^(NSManagedObject *managedObject, RKSearchWord *searchWord, BOOL *stop) {
// Stop the indexing process if we have been cancelled
if ([indexingOperation isCancelled]) *stop = YES;
}];
BOOL performIndexing = YES;
if ([self.delegate respondsToSelector:@selector(searchIndexer:shouldIndexManagedObject:)]) {
performIndexing = [self.delegate searchIndexer:self shouldIndexManagedObject:managedObject];
}
if (performIndexing) {
[self indexManagedObject:managedObject withProgressBlock:^(NSManagedObject *managedObject, RKSearchWord *searchWord, BOOL *stop) {
// Stop the indexing process if we have been cancelled
if ([indexingOperation isCancelled]) *stop = YES;
}];
}
} else {
RKLogError(@"Failed indexing of object %@ with error: %@", managedObject, error);
}

View File

@@ -351,11 +351,10 @@
expect(_objectManager.operationQueue).notTo.beNil();
[_objectManager.operationQueue waitUntilAllOperationsAreFinished];
// Spin the run loop to allow completion blocks to fire after operations have completed
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
expect(progressCallbackCount).to.equal(3);
expect(completionBlockOperationCount).to.equal(3);
dispatch_async(dispatch_get_main_queue(), ^{
expect(progressCallbackCount).to.equal(3);
expect(completionBlockOperationCount).to.equal(3);
});
}
- (void)testThatObjectParametersAreNotSentDuringGetObject

View File

@@ -366,4 +366,149 @@
assertThat([searchWords valueForKey:@"word"], is(empty()));
}
#pragma mark - Delegate Tests
- (void)testThatDelegateCanDenyCreationOfSearchWordForWord
{
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
NSEntityDescription *entity = [[managedObjectModel entitiesByName] objectForKey:@"RKHuman"];
[RKSearchIndexer addSearchIndexingToEntity:entity onAttributes:@[ @"name", @"nickName" ]];
NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
NSError *error;
[persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error];
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;
RKSearchIndexer *indexer = [RKSearchIndexer new];
NSManagedObject *human = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:managedObjectContext];
[human setValue:@"This is my name" forKey:@"name"];
id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKSearchIndexerDelegate)];
BOOL returnValue = NO;
[[[mockDelegate expect] andReturnValue:OCMOCK_VALUE(returnValue)] searchIndexer:OCMOCK_ANY shouldInsertSearchWordForWord:@"this" inManagedObjectContext:managedObjectContext];
returnValue = YES;
[[[mockDelegate expect] andReturnValue:OCMOCK_VALUE(returnValue)] searchIndexer:OCMOCK_ANY shouldInsertSearchWordForWord:@"my" inManagedObjectContext:managedObjectContext];
[[[mockDelegate expect] andReturnValue:OCMOCK_VALUE(returnValue)] searchIndexer:OCMOCK_ANY shouldInsertSearchWordForWord:@"name" inManagedObjectContext:managedObjectContext];
indexer.delegate = mockDelegate;
NSUInteger count = [indexer indexManagedObject:human];
expect(count).to.equal(2);
NSSet *searchWords = [human valueForKey:RKSearchWordsRelationshipName];
NSSet *expectedSet = [NSSet setWithArray:@[ @"my", @"name" ]];
expect([searchWords valueForKey:@"word"]).to.equal(expectedSet);
[mockDelegate verify];
}
- (void)testThatDelegateIsInformedWhenSearchWordIsCreated
{
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
NSEntityDescription *entity = [[managedObjectModel entitiesByName] objectForKey:@"RKHuman"];
[RKSearchIndexer addSearchIndexingToEntity:entity onAttributes:@[ @"name", @"nickName" ]];
NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
NSError *error;
[persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error];
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;
RKSearchIndexer *indexer = [RKSearchIndexer new];
NSManagedObject *human = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:managedObjectContext];
[human setValue:@"This is my name" forKey:@"name"];
id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKSearchIndexerDelegate)];
BOOL returnValue = YES;
[[[mockDelegate stub] andReturnValue:OCMOCK_VALUE(returnValue)] searchIndexer:indexer shouldInsertSearchWordForWord:OCMOCK_ANY inManagedObjectContext:OCMOCK_ANY];
[[mockDelegate expect] searchIndexer:indexer didInsertSearchWord:OCMOCK_ANY forWord:@"this" inManagedObjectContext:managedObjectContext];
[[mockDelegate expect] searchIndexer:indexer didInsertSearchWord:OCMOCK_ANY forWord:@"is" inManagedObjectContext:managedObjectContext];
[[mockDelegate expect] searchIndexer:indexer didInsertSearchWord:OCMOCK_ANY forWord:@"my" inManagedObjectContext:managedObjectContext];
[[mockDelegate expect] searchIndexer:indexer didInsertSearchWord:OCMOCK_ANY forWord:@"name" inManagedObjectContext:managedObjectContext];
indexer.delegate = mockDelegate;
[indexer indexManagedObject:human];
[mockDelegate verify];
}
- (void)testThatDelegateCanBeUsedToFetchExistingSearchWords
{
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
NSEntityDescription *entity = [[managedObjectModel entitiesByName] objectForKey:@"RKHuman"];
[RKSearchIndexer addSearchIndexingToEntity:entity onAttributes:@[ @"name", @"nickName" ]];
NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
NSError *error;
[persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error];
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;
RKSearchIndexer *indexer = [RKSearchIndexer new];
NSManagedObject *human = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:managedObjectContext];
[human setValue:@"This is my name" forKey:@"name"];
id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKSearchIndexerDelegate)];
BOOL returnValue = NO;
RKSearchWord *searchWord = [NSEntityDescription insertNewObjectForEntityForName:@"RKSearchWord" inManagedObjectContext:managedObjectContext];
[[[mockDelegate expect] andReturn:searchWord] searchIndexer:indexer searchWordForWord:@"this" inManagedObjectContext:managedObjectContext error:(NSError * __autoreleasing *)[OCMArg anyPointer]];
[[[mockDelegate expect] andReturn:searchWord] searchIndexer:indexer searchWordForWord:@"is" inManagedObjectContext:managedObjectContext error:(NSError * __autoreleasing *)[OCMArg anyPointer]];
[[[mockDelegate expect] andReturn:searchWord] searchIndexer:indexer searchWordForWord:@"my" inManagedObjectContext:managedObjectContext error:(NSError * __autoreleasing *)[OCMArg anyPointer]];
[[[mockDelegate expect] andReturn:searchWord] searchIndexer:indexer searchWordForWord:@"name" inManagedObjectContext:managedObjectContext error:(NSError * __autoreleasing *)[OCMArg anyPointer]];
[[[mockDelegate stub] andReturnValue:OCMOCK_VALUE(returnValue)] searchIndexer:indexer shouldInsertSearchWordForWord:OCMOCK_ANY inManagedObjectContext:OCMOCK_ANY];
[[mockDelegate reject] searchIndexer:indexer didInsertSearchWord:OCMOCK_ANY forWord:OCMOCK_ANY inManagedObjectContext:OCMOCK_ANY];
indexer.delegate = mockDelegate;
[indexer indexManagedObject:human];
[mockDelegate verify];
}
- (void)testThatTheDelegateCanDeclineIndexingOfAnObject
{
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
NSEntityDescription *entity = [[managedObjectModel entitiesByName] objectForKey:@"RKHuman"];
[RKSearchIndexer addSearchIndexingToEntity:entity onAttributes:@[ @"name", @"nickName" ]];
NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
NSError *error;
[persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error];
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;
RKSearchIndexer *indexer = [RKSearchIndexer new];
NSManagedObject *human = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:managedObjectContext];
[human setValue:@"This is my name" forKey:@"name"];
id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKSearchIndexerDelegate)];
BOOL returnValue = NO;
[[[mockDelegate expect] andReturnValue:OCMOCK_VALUE(returnValue)] searchIndexer:indexer shouldIndexManagedObject:human];
indexer.delegate = mockDelegate;
[indexer indexChangedObjectsInManagedObjectContext:managedObjectContext waitUntilFinished:YES];
[mockDelegate verify];
NSSet *searchWords = [human valueForKey:RKSearchWordsRelationshipName];
expect([searchWords valueForKey:@"word"]).to.beEmpty();
}
- (void)testThatTheDelegateIsNotifiedAfterIndexingHasCompleted
{
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
NSEntityDescription *entity = [[managedObjectModel entitiesByName] objectForKey:@"RKHuman"];
[RKSearchIndexer addSearchIndexingToEntity:entity onAttributes:@[ @"name", @"nickName" ]];
NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
NSError *error;
[persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error];
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;
RKSearchIndexer *indexer = [RKSearchIndexer new];
NSManagedObject *human = [NSEntityDescription insertNewObjectForEntityForName:@"RKHuman" inManagedObjectContext:managedObjectContext];
[human setValue:@"This is my name" forKey:@"name"];
id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(RKSearchIndexerDelegate)];
[[mockDelegate expect] searchIndexer:indexer didIndexManagedObject:human];
indexer.delegate = mockDelegate;
[indexer indexManagedObject:human];
[mockDelegate verify];
}
@end