diff --git a/Signal/test/util/SearcherTest.swift b/Signal/test/util/SearcherTest.swift index 2472a754e..a8b98ca49 100644 --- a/Signal/test/util/SearcherTest.swift +++ b/Signal/test/util/SearcherTest.swift @@ -64,6 +64,10 @@ class FakeContactsManager: NSObject, ContactsManagerProtocol { func isSystemContact(withSignalAccount recipientId: String) -> Bool { return true } + + func compare(signalAccount left: SignalAccount, with right: SignalAccount) -> ComparisonResult { + return .orderedAscending + } } let bobRecipientId = "+49030183000" @@ -265,7 +269,7 @@ class ConversationSearcherTest: XCTestCase { private func getResultSet(searchText: String) -> SearchResultSet { var results: SearchResultSet! self.dbConnection.read { transaction in - results = self.searcher.results(searchText: searchText, transaction: transaction) + results = self.searcher.results(searchText: searchText, transaction: transaction, contactsManager: TextSecureKitEnv.shared().contactsManager) } return results } @@ -342,9 +346,23 @@ class SearcherTest: XCTestCase { XCTAssertFalse(searcher.matches(item: regularLizaveta, query: "Liza 323")) } - func testTextSanitization() { + func testSearchQuery() { + XCTAssertEqual(FullTextSearchFinder.query(searchText: "Liza"), "Liza*") + XCTAssertEqual(FullTextSearchFinder.query(searchText: "Liza +1-323"), "1323* Liza*") + XCTAssertEqual(FullTextSearchFinder.query(searchText: "\"\\ `~!@#$%^&*()_+-={}|[]:;'<>?,./Liza +1-323"), "1323* Liza*") + XCTAssertEqual(FullTextSearchFinder.query(searchText: "renaldo RENALDO reñaldo REÑALDO"), "RENALDO* REÑALDO* renaldo* reñaldo*") + XCTAssertEqual(FullTextSearchFinder.query(searchText: "😏"), "😏*") + XCTAssertEqual(FullTextSearchFinder.query(searchText: "alice 123 bob 456"), "123* 123456* 456* alice* bob*") + XCTAssertEqual(FullTextSearchFinder.query(searchText: "Li!za"), "Liza*") + XCTAssertEqual(FullTextSearchFinder.query(searchText: "Liza Liza"), "Liza*") + XCTAssertEqual(FullTextSearchFinder.query(searchText: "Liza liza"), "Liza* liza*") + } + + func testTextNormalization() { XCTAssertEqual(FullTextSearchFinder.normalize(text: "Liza"), "Liza") - XCTAssertEqual(FullTextSearchFinder.normalize(text: "Liza +1-323"), "Liza 1 323") - XCTAssertEqual(FullTextSearchFinder.normalize(text: "\"\\'!&@#$%^&*()Liza +1-323"), "Liza 1 323") + XCTAssertEqual(FullTextSearchFinder.normalize(text: "Liza +1-323"), "Liza 1323") + XCTAssertEqual(FullTextSearchFinder.normalize(text: "\"\\ `~!@#$%^&*()_+-={}|[]:;'<>?,./Liza +1-323"), "Liza 1323") + XCTAssertEqual(FullTextSearchFinder.normalize(text: "renaldo RENALDO reñaldo REÑALDO"), "renaldo RENALDO reñaldo REÑALDO") + XCTAssertEqual(FullTextSearchFinder.normalize(text: "😏"), "😏") } } diff --git a/SignalMessaging/contacts/OWSContactsManager.h b/SignalMessaging/contacts/OWSContactsManager.h index 7dc66d88c..3a34898ba 100644 --- a/SignalMessaging/contacts/OWSContactsManager.h +++ b/SignalMessaging/contacts/OWSContactsManager.h @@ -71,7 +71,6 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification; * Used for sorting, respects system contacts name sort order preference. */ - (NSString *)comparableNameForSignalAccount:(SignalAccount *)signalAccount; -- (NSComparisonResult)compareSignalAccount:(SignalAccount *)left withSignalAccount:(SignalAccount *)right; // Generally we prefer the formattedProfileName over the raw profileName so as to // distinguish a profile name apart from a name pulled from the system's contacts. diff --git a/SignalMessaging/utils/ConversationSearcher.swift b/SignalMessaging/utils/ConversationSearcher.swift index ff72a4d55..70cb56c63 100644 --- a/SignalMessaging/utils/ConversationSearcher.swift +++ b/SignalMessaging/utils/ConversationSearcher.swift @@ -39,13 +39,13 @@ public class ConversationSearchResult: Comparable { public class ContactSearchResult: Comparable { public let signalAccount: SignalAccount - public let contactsManager: OWSContactsManager + public let contactsManager: ContactsManagerProtocol public var recipientId: String { return signalAccount.recipientId } - init(signalAccount: SignalAccount, contactsManager: OWSContactsManager) { + init(signalAccount: SignalAccount, contactsManager: ContactsManagerProtocol) { self.signalAccount = signalAccount self.contactsManager = contactsManager } @@ -53,7 +53,7 @@ public class ContactSearchResult: Comparable { // Mark: Comparable public static func < (lhs: ContactSearchResult, rhs: ContactSearchResult) -> Bool { - return lhs.contactsManager.compareSignalAccount(lhs.signalAccount, with: rhs.signalAccount) == .orderedAscending + return lhs.contactsManager.compare(signalAccount: lhs.signalAccount, with: rhs.signalAccount) == .orderedAscending } // MARK: Equatable @@ -99,7 +99,7 @@ public class ConversationSearcher: NSObject { public func results(searchText: String, transaction: YapDatabaseReadTransaction, - contactsManager: OWSContactsManager) -> SearchResultSet { + contactsManager: ContactsManagerProtocol) -> SearchResultSet { var conversations: [ConversationSearchResult] = [] var contacts: [ContactSearchResult] = [] diff --git a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h index 64d7e0335..7a6e5cc69 100644 --- a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h +++ b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h @@ -17,6 +17,9 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isSystemContact:(NSString *)recipientId; - (BOOL)isSystemContactWithSignalAccount:(NSString *)recipientId; +- (NSComparisonResult)compareSignalAccount:(SignalAccount *)left + withSignalAccount:(SignalAccount *)right NS_SWIFT_NAME(compare(signalAccount:with:)); + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Storage/FullTextSearchFinder.swift b/SignalServiceKit/src/Storage/FullTextSearchFinder.swift index 467389ee6..afe507080 100644 --- a/SignalServiceKit/src/Storage/FullTextSearchFinder.swift +++ b/SignalServiceKit/src/Storage/FullTextSearchFinder.swift @@ -18,7 +18,7 @@ public class SearchIndexer { } private func normalize(indexingText: String) -> String { - return FullTextSearchFinder.sanitize(text: indexingText) + return FullTextSearchFinder.normalize(text: indexingText) } } @@ -31,19 +31,32 @@ public class FullTextSearchFinder: NSObject { // SQLite does not support suffix or contains matches. public class func query(searchText: String) -> String { // 1. Normalize the search text. - let normalizedSearchText = normalize(queryText: searchText) + let normalizedSearchText = FullTextSearchFinder.normalize(text: searchText) - // 2. Split into tokens. - let queryTerms = normalizedSearchText.split(separator: " ").filter { - // Ignore empty tokens. + // 2. Split into query terms (or tokens). + var queryTerms = normalizedSearchText.split(separator: " ") + + // 3. Add an additional numeric-only query term. + let digitsOnlyScalars = normalizedSearchText.unicodeScalars.lazy.filter { + CharacterSet.decimalDigits.contains($0) + } + let digitsOnly: Substring = Substring(String(String.UnicodeScalarView(digitsOnlyScalars))) + queryTerms.append(digitsOnly) + + // 4. De-duplicate and sort query terms. + queryTerms = Array(Set(queryTerms)).sorted() + + // 5. Filter the query terms. + let filteredQueryTerms = queryTerms.filter { + // Ignore empty terms. $0.count > 0 }.map { - // Allow partial match of each token. + // Allow partial match of each term. $0 + "*" } - // 3. Join tokens into query string. - let query = queryTerms.joined(separator: " ") + // 6. Join terms into query string. + let query = filteredQueryTerms.joined(separator: " ") return query } @@ -73,13 +86,13 @@ public class FullTextSearchFinder: NSObject { } } - // Mark: Filtering + // Mark: Normalization fileprivate class func charactersToRemove() -> CharacterSet { var charactersToFilter = CharacterSet.punctuationCharacters charactersToFilter.formUnion(CharacterSet.illegalCharacters) charactersToFilter.formUnion(CharacterSet.controlCharacters) - charactersToFilter.formUnion(CharacterSet.symbols) + charactersToFilter.formUnion(CharacterSet(charactersIn: "+~$^=|<>`")) return charactersToFilter } @@ -88,7 +101,7 @@ public class FullTextSearchFinder: NSObject { return separatorCharacters } - fileprivate class func sanitize(text: String) -> String { + public class func normalize(text: String) -> String { // 1. Filter out invalid characters. let filtered = text.unicodeScalars.lazy.filter({ !charactersToRemove().contains($0) @@ -115,21 +128,6 @@ public class FullTextSearchFinder: NSObject { return result.trimmingCharacters(in: .whitespacesAndNewlines) } - private class func normalize(queryText: String) -> String { - var normalized: String = FullTextSearchFinder.sanitize(text: queryText) - - let digitsOnlyScalars = normalized.unicodeScalars.lazy.filter { - CharacterSet.decimalDigits.contains($0) - } - let normalizedDigits = String(String.UnicodeScalarView(digitsOnlyScalars)) - - if normalizedDigits.count > 0 { - return "\(normalized) OR \(normalizedDigits)" - } else { - return normalized - } - } - // Mark: Index Building private class var contactsManager: ContactsManagerProtocol {