mirror of
https://github.com/zhigang1992/RestKit.git
synced 2026-04-06 09:41:09 +08:00
382 lines
18 KiB
Objective-C
382 lines
18 KiB
Objective-C
//
|
|
// RKSearchIndexer.m
|
|
// RestKit
|
|
//
|
|
// Created by Blake Watters on 7/27/12.
|
|
// Copyright (c) 2012 RestKit. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
#import "RKSearchIndexer.h"
|
|
#import "RKSearchWordEntity.h"
|
|
#import "RKSearchWord.h"
|
|
#import "RKLog.h"
|
|
#import "RKSearchTokenizer.h"
|
|
#import "NSManagedObjectContext+RKAdditions.h"
|
|
|
|
// Set Logging Component
|
|
#undef RKLogComponent
|
|
#define RKLogComponent RKlcl_cRestKitSearch
|
|
|
|
NSString * const RKSearchableAttributeNamesUserInfoKey = @"RestKitSearchableAttributes";
|
|
|
|
@interface RKSearchIndexer ()
|
|
@property (nonatomic, strong) NSOperationQueue *operationQueue;
|
|
@property (nonatomic, assign) NSUInteger totalIndexingOperationCount;
|
|
@end
|
|
|
|
@implementation RKSearchIndexer
|
|
|
|
+ (void)addSearchIndexingToEntity:(NSEntityDescription *)entity onAttributes:(NSArray *)attributes
|
|
{
|
|
NSParameterAssert(entity);
|
|
NSParameterAssert(attributes);
|
|
|
|
// Create a relationship from the RKSearchWordEntity to the given searchable entity
|
|
NSEntityDescription *searchWordEntity = [[entity.managedObjectModel entitiesByName] objectForKey:RKSearchWordEntityName];
|
|
if (! searchWordEntity) {
|
|
searchWordEntity = [[RKSearchWordEntity alloc] init];
|
|
|
|
// Add the entity to the model
|
|
NSArray *entities = [entity.managedObjectModel entities];
|
|
[entity.managedObjectModel setEntities:[entities arrayByAddingObject:searchWordEntity]];
|
|
}
|
|
|
|
NSMutableArray *attributeNames = [NSMutableArray arrayWithCapacity:[attributes count]];
|
|
for (id attributeIdentifier in attributes) {
|
|
NSAttributeDescription *attribute = nil;
|
|
if ([attributeIdentifier isKindOfClass:[NSString class]]) {
|
|
// Look it up by name
|
|
attribute = [[entity attributesByName] objectForKey:attributeIdentifier];
|
|
NSAssert(attribute, @"Invalid attribute identifier given: No attribute with the name '%@' found in the '%@' entity.", attributeIdentifier, entity.name);
|
|
} else if ([attributeIdentifier isKindOfClass:[NSAttributeDescription class]]) {
|
|
attribute = attributeIdentifier;
|
|
} else {
|
|
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
|
reason:[NSString stringWithFormat:@"Unable to configure search indexing: Invalid attribute identifier of type '%@' given, expected an NSString or NSAttributeDescription. (Value: %@)", [attributeIdentifier class], attributeIdentifier]
|
|
userInfo:nil];
|
|
}
|
|
|
|
NSAssert(attribute.attributeType == NSStringAttributeType, @"Invalid attribute identifier given: Expected an attribute of type NSStringAttributeType, got %ld.", (unsigned long) attribute.attributeType);
|
|
[attributeNames addObject:attribute.name];
|
|
}
|
|
|
|
// Store the searchable attributes into the user info dictionary
|
|
NSMutableDictionary *userInfo = [[entity userInfo] mutableCopy];
|
|
[userInfo setObject:attributeNames forKey:RKSearchableAttributeNamesUserInfoKey];
|
|
[entity setUserInfo:userInfo];
|
|
|
|
// Create a relationship from our indexed entity to the RKSearchWord entity
|
|
NSRelationshipDescription *relationship = [[NSRelationshipDescription alloc] init];
|
|
[relationship setName:RKSearchWordsRelationshipName];
|
|
[relationship setDestinationEntity:searchWordEntity];
|
|
[relationship setMaxCount:0]; // Make it to-many
|
|
[relationship setDeleteRule:NSNullifyDeleteRule];
|
|
|
|
NSArray *properties = [entity properties];
|
|
[entity setProperties:[properties arrayByAddingObject:relationship]];
|
|
|
|
// Create an inverse relationship from the searchWords to the searchable entity
|
|
NSRelationshipDescription *inverseRelationship = [[NSRelationshipDescription alloc] init];
|
|
[inverseRelationship setName:entity.name];
|
|
[inverseRelationship setDestinationEntity:entity];
|
|
[inverseRelationship setDeleteRule:NSNullifyDeleteRule];
|
|
NSArray *searchWordProperties = [searchWordEntity properties];
|
|
[searchWordEntity setProperties:[searchWordProperties arrayByAddingObject:inverseRelationship]];
|
|
|
|
// Connect the relationships as inverses
|
|
[relationship setInverseRelationship:inverseRelationship];
|
|
[inverseRelationship setInverseRelationship:relationship];
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
// Setup serial operation queue to enable cancellation of indexing
|
|
self.operationQueue = [NSOperationQueue new];
|
|
self.operationQueue.maxConcurrentOperationCount = 1;
|
|
[self.operationQueue addObserver:self forKeyPath:@"operationCount" options:0 context:NULL];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self cancelAllIndexingOperations];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)startObservingManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
|
|
{
|
|
NSParameterAssert(managedObjectContext);
|
|
|
|
if (self.indexingContext) {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(handleManagedObjectContextDidSaveNotification:)
|
|
name:NSManagedObjectContextDidSaveNotification
|
|
object:managedObjectContext];
|
|
} else {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(handleManagedObjectContextWillSaveNotification:)
|
|
name:NSManagedObjectContextWillSaveNotification
|
|
object:managedObjectContext];
|
|
}
|
|
}
|
|
|
|
- (void)stopObservingManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
|
|
{
|
|
NSParameterAssert(managedObjectContext);
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:managedObjectContext];
|
|
}
|
|
|
|
- (NSUInteger)indexManagedObject:(NSManagedObject *)managedObject withProgressBlock:(void (^)(NSManagedObject *managedObject, RKSearchWord *searchWord, BOOL *stop))progressBlock;
|
|
{
|
|
@autoreleasepool {
|
|
|
|
RKLogDebug(@"Indexing searchable attributes of managed object: %@", managedObject);
|
|
NSArray *searchableAttributes = [managedObject.entity.userInfo objectForKey:RKSearchableAttributeNamesUserInfoKey];
|
|
if (! searchableAttributes) {
|
|
[NSException raise:NSInvalidArgumentException format:@"The given managed object %@ is for an entity (%@) that does not define any searchable attributes. Perhaps you forgot to invoke addSearchIndexingToEntity:onAttributes:?", managedObject, managedObject.entity];
|
|
return NSNotFound;
|
|
}
|
|
|
|
RKSearchTokenizer *searchTokenizer = [RKSearchTokenizer new];
|
|
searchTokenizer.stopWords = self.stopWords;
|
|
|
|
__block NSUInteger searchWordCount;
|
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:RKSearchWordEntityName];
|
|
fetchRequest.fetchLimit = 1;
|
|
NSPredicate *predicateTemplate = [NSPredicate predicateWithFormat:@"%K == $SEARCH_WORD", RKSearchWordAttributeName];
|
|
NSManagedObjectContext *managedObjectContext = managedObject.managedObjectContext;
|
|
__block BOOL stop = NO;
|
|
|
|
[managedObjectContext performBlockAndWait:^{
|
|
NSMutableSet *searchWords = [NSMutableSet set];
|
|
for (NSString *searchableAttribute in searchableAttributes) {
|
|
NSString *attributeValue = [managedObject valueForKey:searchableAttribute];
|
|
if (attributeValue) {
|
|
RKLogTrace(@"Generating search words for searchable attribute: %@", searchableAttribute);
|
|
NSSet *tokens = [searchTokenizer tokenize:attributeValue];
|
|
for (NSString *word in tokens) {
|
|
if (word && [word length] > 0) {
|
|
fetchRequest.predicate = [predicateTemplate predicateWithSubstitutionVariables:@{ @"SEARCH_WORD" : word }];
|
|
NSError *error = nil;
|
|
NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
|
if (results) {
|
|
RKSearchWord *searchWord;
|
|
if ([results count] == 0) {
|
|
searchWord = [NSEntityDescription insertNewObjectForEntityForName:RKSearchWordEntityName inManagedObjectContext:managedObjectContext];
|
|
searchWord.word = word;
|
|
} else {
|
|
searchWord = [results objectAtIndex:0];
|
|
}
|
|
|
|
NSAssert([[searchWord managedObjectContext] isEqual:managedObjectContext], @"Serious Core Data error: Expected `NSManagedObject` for the 'RKSearchWord' entity in context %@, but got one in %@", managedObject, [searchWord managedObjectContext]);
|
|
[searchWords addObject:searchWord];
|
|
|
|
if (progressBlock) progressBlock(managedObject, searchWord, &stop);
|
|
} else {
|
|
RKLogError(@"Failed to retrieve search word: %@", error);
|
|
}
|
|
}
|
|
|
|
if (stop) break;
|
|
}
|
|
}
|
|
|
|
if (stop) break;
|
|
}
|
|
|
|
if (! stop) {
|
|
[managedObject setValue:searchWords forKey:RKSearchWordsRelationshipName];
|
|
RKLogTrace(@"Indexed search words: %@", [searchWords valueForKey:RKSearchWordAttributeName]);
|
|
searchWordCount = [searchWords count];
|
|
}
|
|
}];
|
|
|
|
return searchWordCount;
|
|
}
|
|
}
|
|
|
|
- (NSUInteger)indexManagedObject:(NSManagedObject *)managedObject
|
|
{
|
|
return [self indexManagedObject:managedObject withProgressBlock:nil];
|
|
}
|
|
|
|
/**
|
|
NOTE: Does **NOT** use the indexing context as unsaved objects would not be available for indexing in that context
|
|
*/
|
|
- (void)indexChangedObjectsInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
|
|
waitUntilFinished:(BOOL)wait
|
|
{
|
|
NSParameterAssert(managedObjectContext);
|
|
|
|
NSSet *candidateObjects = [[NSSet setWithSet:managedObjectContext.insertedObjects] setByAddingObjectsFromSet:managedObjectContext.updatedObjects];
|
|
NSSet *objectsToIndex = [self objectsToIndexFromCandidateObjects:candidateObjects checkChangedValues:YES];
|
|
|
|
if (wait) {
|
|
// Synchronous indexing
|
|
NSUInteger totalObjects = [objectsToIndex count];
|
|
__block NSMutableSet *indexedIDs = [NSMutableSet setWithCapacity:totalObjects];
|
|
for (NSManagedObject *managedObject in objectsToIndex) {
|
|
[self indexManagedObject:managedObject withProgressBlock:^(NSManagedObject *managedObject, RKSearchWord *searchWord, BOOL *stop) {
|
|
if (totalObjects < 250) return;
|
|
if ([indexedIDs containsObject:[managedObject objectID]]) return;
|
|
[indexedIDs addObject:[managedObject objectID]];
|
|
double percentage = (((float)[indexedIDs count]) / (float)totalObjects) * 100;
|
|
if ([indexedIDs count] % 250 == 0) RKLogInfo(@"Indexing object %ld of %ld (%.2f%% complete)", (unsigned long) [indexedIDs count], (unsigned long) totalObjects, percentage);
|
|
}];
|
|
}
|
|
|
|
if (totalObjects >= 250) RKLogInfo(@"Finished indexing.");
|
|
} else {
|
|
// Perform asynchronous indexing
|
|
for (NSManagedObject *managedObject in objectsToIndex) {
|
|
[self.operationQueue addOperationWithBlock:^{
|
|
[self indexManagedObject:managedObject];
|
|
}];
|
|
}
|
|
self.totalIndexingOperationCount = [self.operationQueue operationCount];
|
|
}
|
|
}
|
|
|
|
- (void)indexChangedObjectsFromManagedObjectContextDidSaveNotification:(NSNotification *)notification
|
|
{
|
|
if (! self.indexingContext) {
|
|
RKLogWarning(@"Received `NSManagedObjectContextDidSaveNotification` with nil indexing context: ignoring...");
|
|
return;
|
|
}
|
|
|
|
NSDictionary *userInfo = [notification userInfo];
|
|
NSSet *candidateObjects = [[NSSet setWithSet:[userInfo objectForKey:NSInsertedObjectsKey]] setByAddingObjectsFromSet:[userInfo objectForKey:NSUpdatedObjectsKey]];
|
|
NSSet *objectsToIndex = [self objectsToIndexFromCandidateObjects:candidateObjects checkChangedValues:NO];
|
|
|
|
// After all indexing is complete, save the indexing context
|
|
__block NSBlockOperation *saveOperation = [NSBlockOperation blockOperationWithBlock:^{
|
|
if ([saveOperation isCancelled]) return;
|
|
[self.indexingContext performBlockAndWait:^{
|
|
NSError *error = nil;
|
|
RKLogInfo(@"Indexing completed. Saving indexing context...");
|
|
BOOL success = [self.indexingContext saveToPersistentStore:&error];
|
|
if (! success) {
|
|
RKLogError(@"Failed to save indexing context: %@", error);
|
|
}
|
|
}];
|
|
}];
|
|
|
|
NSMutableSet *failedObjectIDs = [NSMutableSet set];
|
|
|
|
// Enqueue an operation for each object to index
|
|
NSArray *objectIDsForObjectsToIndex = [objectsToIndex valueForKey:@"objectID"];
|
|
for (NSManagedObjectID *objectID in objectIDsForObjectsToIndex) {
|
|
__block NSBlockOperation *indexingOperation = [NSBlockOperation blockOperationWithBlock:^{
|
|
if ([indexingOperation isCancelled]) return;
|
|
[self.indexingContext performBlockAndWait:^{
|
|
NSError *error = nil;
|
|
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;
|
|
}];
|
|
} else {
|
|
RKLogError(@"Failed indexing of object %@ with error: %@", managedObject, error);
|
|
}
|
|
}];
|
|
}];
|
|
|
|
[saveOperation addDependency:indexingOperation];
|
|
[self.operationQueue addOperation:indexingOperation];
|
|
}
|
|
|
|
// Assert that we indexed everything sucessfully
|
|
[self.operationQueue addOperationWithBlock:^{
|
|
NSAssert([failedObjectIDs count] == 0, @"Expected no indexing failures, got %ld", (long) [failedObjectIDs count]);
|
|
}];
|
|
|
|
[self.operationQueue addOperation:saveOperation];
|
|
self.totalIndexingOperationCount = [self.operationQueue operationCount];
|
|
}
|
|
|
|
- (void)cancelAllIndexingOperations
|
|
{
|
|
[self.operationQueue cancelAllOperations];
|
|
}
|
|
|
|
- (void)waitUntilAllIndexingOperationsAreFinished
|
|
{
|
|
[self.operationQueue waitUntilAllOperationsAreFinished];
|
|
}
|
|
|
|
#pragma mark - Private
|
|
|
|
- (void)handleManagedObjectContextWillSaveNotification:(NSNotification *)notification
|
|
{
|
|
NSManagedObjectContext *managedObjectContext = [notification object];
|
|
RKLogInfo(@"Managed object context will save notification received. Checking changed and inserted objects for searchable entities...");
|
|
|
|
// We wait until finished to ensure that the indexed objects are persisted with the save
|
|
[self indexChangedObjectsInManagedObjectContext:managedObjectContext waitUntilFinished:YES];
|
|
}
|
|
|
|
- (void)handleManagedObjectContextDidSaveNotification:(NSNotification *)notification
|
|
{
|
|
RKLogInfo(@"Managed object context did save notification received. Checking changed and inserted objects for searchable entities...");
|
|
[self indexChangedObjectsFromManagedObjectContextDidSaveNotification:notification];
|
|
}
|
|
|
|
- (NSSet *)objectsToIndexFromCandidateObjects:(NSSet *)objects checkChangedValues:(BOOL)checkChangedValues
|
|
{
|
|
NSUInteger totalObjects = [objects count];
|
|
NSMutableSet *objectsNeedingIndexing = [[NSMutableSet alloc] initWithCapacity:totalObjects];
|
|
RKLogInfo(@"Indexing %ld changed objects", (unsigned long) totalObjects);
|
|
|
|
for (NSManagedObject *managedObject in objects) {
|
|
NSArray *searchableAttributes = [managedObject.entity.userInfo objectForKey:RKSearchableAttributeNamesUserInfoKey];
|
|
if (! searchableAttributes) {
|
|
RKLogTrace(@"Skipping indexing for managed object for entity '%@': no searchable attributes found.", managedObject.entity.name);
|
|
continue;
|
|
}
|
|
|
|
for (NSString *attribute in searchableAttributes) {
|
|
if (!checkChangedValues || [[managedObject changedValues] objectForKey:attribute]) {
|
|
RKLogTrace(@"Detected change to searchable attribute '%@' for managed object '%@': updating search index.", attribute, managedObject);
|
|
[objectsNeedingIndexing addObject:managedObject];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return objectsNeedingIndexing;
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
|
{
|
|
if ([keyPath isEqualToString:@"operationCount"]) {
|
|
if (self.totalIndexingOperationCount > 0 && self.operationQueue.operationCount > 0) {
|
|
NSUInteger index = self.totalIndexingOperationCount - self.operationQueue.operationCount;
|
|
double percentage = (((float)index) / (float)self.totalIndexingOperationCount) * 100;
|
|
if (index % 250 == 0) RKLogInfo(@"Indexing object %ld of %ld (%.2f%% complete)", (unsigned long) index, (unsigned long) self.totalIndexingOperationCount, percentage);
|
|
if (self.operationQueue.operationCount == 0) {
|
|
if (self.totalIndexingOperationCount >= 250) RKLogInfo(@"Finished indexing.");
|
|
self.totalIndexingOperationCount = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|