diff --git a/Source/API/CBLQuery+FullTextSearch.h b/Source/API/CBLQuery+FullTextSearch.h index 44bdecaa6..3b6e0e90a 100644 --- a/Source/API/CBLQuery+FullTextSearch.h +++ b/Source/API/CBLQuery+FullTextSearch.h @@ -37,6 +37,18 @@ They also can't be reduced or grouped, so those properties are ignored too. */ @property (copy, nullable) NSString* fullTextQuery; +/** If set to YES, the query will collect snippets of the text surrounding each match, available + via the CBLFullTextQueryRow's -snippetWithWordStart:wordEnd: method. + (NOTE: ForestDB currently does not support snippets.) */ +@property BOOL fullTextSnippets; + +/** If YES (the default) the full-text query result rows will be sorted by (approximate) relevance. + If set to NO, the rows will be returned in the order the documents were added to the database, + i.e. essentially unordered; this is somewhat faster, so it can be useful if you don't care + about the ordering of the rows. + (NOTE: ForestDB currently does not support ranking.) */ +@property BOOL fullTextRanking; + @end @@ -49,6 +61,15 @@ match(es). */ @property (readonly, nullable) NSString* fullText; +/** Returns a short substring of the full text containing at least some of the matched words. + This is useful to display in search results, and is faster than fetching the .fullText. + NOTE: The "fullTextSnippets" property of the CBLQuery must be set to YES to enable this; + otherwise the result will be nil. + @param wordStart A delimiter that will be inserted before every instance of a match. + @param wordEnd A delimiter that will be inserted after every instance of a match. */ +- (NSString*) snippetWithWordStart: (NSString*)wordStart + wordEnd: (NSString*)wordEnd; + /** The number of query words that were found in the fullText. (If a query word appears more than once, only the first instance is counted.) */ @property (readonly) NSUInteger matchCount; diff --git a/Source/API/CBLQuery+FullTextSearch.m b/Source/API/CBLQuery+FullTextSearch.m index 66110070b..1557d9f3c 100644 --- a/Source/API/CBLQuery+FullTextSearch.m +++ b/Source/API/CBLQuery+FullTextSearch.m @@ -36,9 +36,11 @@ - (void) setFullTextRanking:(BOOL)fullTextRanking {_fullTextRanking = fullText @implementation CBLFullTextQueryRow { UInt64 _fullTextID; + NSString* _snippet; NSMutableArray* _matchOffsets; } +@synthesize snippet=_snippet; - (instancetype) initWithDocID: (NSString*)docID sequence: (SequenceNumber)sequence @@ -92,11 +94,27 @@ - (NSRange) textRangeOfMatch: (NSUInteger)matchNumber { } +- (NSString*) snippetWithWordStart: (NSString*)wordStart + wordEnd: (NSString*)wordEnd +{ + if (!_snippet) + return nil; + NSMutableString* snippet = [_snippet mutableCopy]; + [snippet replaceOccurrencesOfString: @"\001" withString: wordStart + options:NSLiteralSearch range:NSMakeRange(0, snippet.length)]; + [snippet replaceOccurrencesOfString: @"\002" withString: wordEnd + options:NSLiteralSearch range:NSMakeRange(0, snippet.length)]; + return snippet; +} + + // Overridden to add FTS result info - (NSDictionary*) asJSONDictionary { NSMutableDictionary* dict = [[super asJSONDictionary] mutableCopy]; if (!dict[@"error"]) { [dict removeObjectForKey: @"key"]; + if (_snippet) + dict[@"snippet"] = [self snippetWithWordStart: @"[" wordEnd: @"]"]; if (_matchOffsets) { NSMutableArray* matches = [[NSMutableArray alloc] init]; for (NSUInteger i = 0; i < _matchOffsets.count; i += 4) { diff --git a/Source/API/CouchbaseLitePrivate.h b/Source/API/CouchbaseLitePrivate.h index 859402992..6ac8697fb 100644 --- a/Source/API/CouchbaseLitePrivate.h +++ b/Source/API/CouchbaseLitePrivate.h @@ -160,6 +160,7 @@ fullTextID: (UInt64)fullTextID value: (id)value storage: (id)storage; +@property (copy) NSString* snippet; - (void) addTerm: (NSUInteger)term atRange: (NSRange)range; @end diff --git a/Source/CBL_SQLiteViewStorage.m b/Source/CBL_SQLiteViewStorage.m index 0f55280cc..4aece621c 100644 --- a/Source/CBL_SQLiteViewStorage.m +++ b/Source/CBL_SQLiteViewStorage.m @@ -881,8 +881,8 @@ - (CBLQueryIteratorBlock) fullTextQueryWithOptions: (const CBLQueryOptions*)opti [row addTerm: term atRange: NSMakeRange(location, length)]; } -// if (options->fullTextSnippets) -// row.snippet = [r stringForColumnIndex: 5]; + if (options->fullTextSnippets) + row.snippet = [r stringForColumnIndex: 5]; if (!options.filter || options.filter(row)) [rows addObject: row]; } diff --git a/Unit-Tests/ViewInternal_Tests.m b/Unit-Tests/ViewInternal_Tests.m index e78e52b8e..f25ec7c53 100644 --- a/Unit-Tests/ViewInternal_Tests.m +++ b/Unit-Tests/ViewInternal_Tests.m @@ -1199,10 +1199,12 @@ - (void) test23_FullTextQuery { AssertEq(rows.count, 0u); } -#if 0 // Boolean operators and snippets are not available (yet) with ForestDB + - (void) test24_FullTextQuery_Advanced { + if (!self.isSQLiteDB) + return; // Boolean operators and snippets are not available (yet) with ForestDB + RequireTestCase(CBL_View_FullTextQuery); - CBLDatabase *db = createDB(); CBLStatus status; NSMutableArray* docs = $marray(); @@ -1213,10 +1215,9 @@ - (void) test24_FullTextQuery_Advanced { [docs addObject: [self putDoc: $dict({@"_id", @"55555"}, {@"text", @"was barking."})]]; CBLView* view = [db viewNamed: @"fts"]; - view.indexType = kCBLFullTextIndex; [view setMapBlock: MAPBLOCK({ if (doc[@"text"]) - emit(doc[@"text"], doc[@"_id"]); + emit(CBLTextKey(doc[@"text"]), doc[@"_id"]); }) reduceBlock: NULL version: @"1"]; AssertEq([view updateIndex], kCBLStatusOK); @@ -1229,21 +1230,21 @@ - (void) test24_FullTextQuery_Advanced { Assert(rowIter, @"_queryFullText failed: %d", status); Log(@"rows = %@", rowIter); NSArray* expectedRows = $array($dict({@"id", @"44444"}, - {@"matches", @[@{@"range": @[@4, @7], @"term": @0}]}, + {@"matches", @[@{@"range": @[@4, @6], @"term": @0}]}, {@"snippet", @"and [STöRMy] night."}, {@"value", @"44444"}), $dict({@"id", @"33333"}, {@"matches", @[@{@"range": @[@2, @3], @"term": @1}, - @{@"range": @[@26, @3], @"term": @1}]}, + @{@"range": @[@22, @3], @"term": @1}]}, {@"snippet", @"a [dog] whøse ñame was “[Dog]”"}, {@"value", @"33333"})); - AssertEqual(rowsToDicts(rowIter), expectedRows); + AssertEqualish(rowsToDicts(rowIter), expectedRows); // Try a query with snippets: CBLQuery* query = [view createQuery]; query.fullTextQuery = @"(was NOT barking) OR dog"; query.fullTextSnippets = YES; - rows = [[query run: NULL] allObjects]; + NSArray* rows = [[query run: NULL] allObjects]; AssertEq(rows.count, 2u); CBLFullTextQueryRow* row = rows[0]; @@ -1287,12 +1288,11 @@ - (void) test24_FullTextQuery_Advanced { Log(@"after deletion, rows = %@", rowIter); expectedRows = $array($dict({@"id", @"44444"}, - {@"matches", @[@{@"range": @[@4, @7], @"term": @0}]}, + {@"matches", @[@{@"range": @[@4, @6], @"term": @0}]}, {@"snippet", @"and [STöRMy] night."}, {@"value", @"44444"})); - AssertEqual(rowsToDicts(rowIter), expectedRows); + AssertEqualish(rowsToDicts(rowIter), expectedRows); } -#endif - (void) test25_TotalDocs {