Search Performance Optimization

This commit is contained in:
Chris Li 2016-07-14 10:52:13 -04:00
parent 8e0eb87959
commit 70f3c9d777
10 changed files with 79 additions and 25 deletions

View File

@ -9,9 +9,10 @@
import UIKit
class SettingTBVC: UITableViewController {
private(set) var sectionHeader = [LocalizedStrings.library, LocalizedStrings.reading,LocalizedStrings.misc]
private(set) var sectionHeader = [LocalizedStrings.library, LocalizedStrings.reading, LocalizedStrings.search, LocalizedStrings.misc]
private(set) var cellTextlabels = [[LocalizedStrings.libraryAutoRefresh, LocalizedStrings.libraryUseCellularData, LocalizedStrings.libraryBackup],
[LocalizedStrings.fontSize, LocalizedStrings.adjustLayout],
[LocalizedStrings.history],
[LocalizedStrings.rateKiwix, LocalizedStrings.about]]
let dateComponentsFormatter: NSDateComponentsFormatter = {
@ -27,8 +28,7 @@ class SettingTBVC: UITableViewController {
showRateKiwixIfNeeded()
if UIApplication.buildStatus == .Alpha {
sectionHeader.append("Search")
cellTextlabels.append(["Boost Factor 🚀"])
cellTextlabels[2].append("Boost Factor 🚀")
}
}

View File

@ -9,7 +9,7 @@
import UIKit
import DZNEmptyDataSet
class TableOfContentsController: UIViewController, UITableViewDelegate, UITableViewDataSource ,DZNEmptyDataSetSource, DZNEmptyDataSetDelegate {
class TableOfContentsController: UIViewController, UITableViewDelegate, UITableViewDataSource, DZNEmptyDataSetSource, DZNEmptyDataSetDelegate {
@IBOutlet weak var tableView: UITableView!
weak var delegate: TableOfContentsDelegate?

View File

@ -36,7 +36,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.6.1529</string>
<string>1.6.1563</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@ -42,7 +42,7 @@ enum BuildStatus {
extension UIApplication {
class var buildStatus: BuildStatus {
get {
return .Alpha
return .Beta
}
}
}

View File

@ -12,6 +12,7 @@ import PSOperations
class SearchOperation: GroupOperation {
let completionHandler: ([SearchResult]?) -> Void
private(set) var results = [SearchResult]()
//private let startTime = NSDate()
init(searchTerm: String, completionHandler: ([SearchResult]?) -> Void) {
self.completionHandler = completionHandler
@ -26,7 +27,7 @@ class SearchOperation: GroupOperation {
guard let book = Book.fetch(id, context: managedObjectContext) else {continue}
guard book.includeInSearch else {continue}
let operation = SingleBookSearchOperation(zimReader: zimReader,
searchTerm: searchTerm.lowercaseString,
lowerCaseSearchTerm: searchTerm.lowercaseString,
completionHandler: { [unowned sortOperation] (results) in
sortOperation.results += results
})
@ -41,7 +42,7 @@ class SearchOperation: GroupOperation {
}
override func finished(errors: [NSError]) {
// print("Search Operation finished, status \(cancelled ? "Canceled" : "Not Canceled")")
//print("Search Operation finished, status \(cancelled ? "Canceled" : "Not Canceled"), \(NSDate().timeIntervalSinceDate(startTime))")
NSOperationQueue.mainQueue().addOperationWithBlock {
self.completionHandler(self.cancelled ? nil : self.results)
}
@ -50,22 +51,22 @@ class SearchOperation: GroupOperation {
private class SingleBookSearchOperation: Operation {
let zimReader: ZimReader
let searchTerm: String
let lowerCaseSearchTerm: String
let completionHandler: ([SearchResult]) -> Void
init(zimReader: ZimReader, searchTerm: String, completionHandler: ([SearchResult]) -> Void) {
init(zimReader: ZimReader, lowerCaseSearchTerm: String, completionHandler: ([SearchResult]) -> Void) {
self.zimReader = zimReader
self.searchTerm = searchTerm
self.lowerCaseSearchTerm = lowerCaseSearchTerm
self.completionHandler = completionHandler
}
override private func execute() {
var results = [String: SearchResult]()
let indexedDics = zimReader.searchUsingIndex(searchTerm) as? [[String: AnyObject]] ?? [[String: AnyObject]]()
let titleDics = zimReader.searchSuggestionsSmart(searchTerm) as? [[String: AnyObject]] ?? [[String: AnyObject]]()
let indexedDics = zimReader.searchUsingIndex(lowerCaseSearchTerm) as? [[String: AnyObject]] ?? [[String: AnyObject]]()
let titleDics = zimReader.searchSuggestionsSmart(lowerCaseSearchTerm) as? [[String: AnyObject]] ?? [[String: AnyObject]]()
let mixedDics = titleDics + indexedDics // It is important we process the title search result first, so that we always keep the indexed search result
for dic in mixedDics {
guard let result = SearchResult (rawResult: dic) else {continue}
guard let result = SearchResult (rawResult: dic, lowerCaseSearchTerm: lowerCaseSearchTerm) else {continue}
results[result.path] = result
}
completionHandler(Array(results.values))

View File

@ -63,6 +63,7 @@ class LocalizedStrings {
class var remove: String {return NSLocalizedString("Remove", comment: "Basic")}
class var delete: String {return NSLocalizedString("Delete", comment: "Basic")}
class var refreshing: String {return NSLocalizedString("Refreshing...", comment: "Basic")}
class var history: String {return NSLocalizedString("History", comment: "Basic")}
// MARK: - OS X
class var General: String {return NSLocalizedString("General", comment: "OS X, Preference")}

View File

@ -8,6 +8,9 @@
import UIKit
typealias ZimID = String
typealias ArticlePath = String
extension ZimReader {
var metaData: [String: AnyObject] {
var metadata = [String: AnyObject]()
@ -28,5 +31,46 @@ extension ZimReader {
}
}
typealias ZimID = String
typealias ArticlePath = String
// https://gist.github.com/adamyanalunas/69f6601fad6040686d300a1cdc20f500
private extension String {
subscript(index: Int) -> Character {
return self[startIndex.advancedBy(index)]
}
subscript(range: Range<Int>) -> String {
let start = startIndex.advancedBy(range.startIndex)
let end = startIndex.advancedBy(range.endIndex)
return self[start..<end]
}
}
extension String {
func levenshtein(string cmpString: String) -> Int {
let (length, cmpLength) = (characters.count, cmpString.characters.count)
var matrix = Array(
count: cmpLength + 1,
repeatedValue: Array(
count: length + 1,
repeatedValue: 0
)
)
for m in 1..<cmpLength {
matrix[m][0] = matrix[m - 1][0] + 1
}
for n in 1..<length {
matrix[0][n] = matrix[0][n - 1] + 1
}
for m in 1..<(cmpLength + 1) {
for n in 1..<(length + 1) {
let penalty = self[n - 1] == cmpString[m - 1] ? 0 : 1
let (horizontal, vertical, diagonal) = (matrix[m - 1][n] + 1, matrix[m][n - 1] + 1, matrix[m - 1][n - 1])
matrix[m][n] = min(horizontal, vertical, diagonal + penalty)
}
}
return matrix[cmpLength][length]
}
}

View File

@ -9,13 +9,18 @@
import UIKit
class SearchResult: CustomStringConvertible {
let lowerCaseSearchTerm: String
let title: String
let path: ArticlePath
let bookID: ZimID
let snippet: String?
let probability: Double? // range: 0.0 - 1.0
let distance: Int // Levenshtein distance, non negative integer
private(set) lazy var distance: Int = {
// Here we dont use the swift version of levenshtein, because it is slower than the C++ implementation
//return self.title.lowercaseString.levenshtein(string: self.lowerCaseSearchTerm)
return ZimReader.levenshtein(self.title.lowercaseString, anotherString: self.lowerCaseSearchTerm)
}()
private(set) lazy var score: Double = {
if let probability = self.probability {
return WeightFactor.calculate(probability) * Double(self.distance)
@ -24,13 +29,13 @@ class SearchResult: CustomStringConvertible {
}
}()
init?(rawResult: [String: AnyObject]) {
init?(rawResult: [String: AnyObject], lowerCaseSearchTerm: String) {
self.lowerCaseSearchTerm = lowerCaseSearchTerm
let title = (rawResult["title"] as? String) ?? ""
let path = (rawResult["path"] as? String) ?? ""
let bookID = (rawResult["bookID"] as? ZimID) ?? ""
let snippet = rawResult["snippet"] as? String
let distance = (rawResult["distance"]as? NSNumber)?.integerValue ?? title.characters.count
let probability: Double? = {
if let probability = (rawResult["probability"] as? NSNumber)?.doubleValue {
return probability / 100.0
@ -44,7 +49,6 @@ class SearchResult: CustomStringConvertible {
self.bookID = bookID
self.snippet = snippet
self.probability = probability
self.distance = distance
if title == "" || path == "" || bookID == "" {return nil}
}

View File

@ -54,4 +54,6 @@
- (NSURL *)fileURL;
+ (NSInteger)levenshtein:(NSString *)string anotherString:(NSString *)anotherString;
@end

View File

@ -57,11 +57,9 @@
while (_reader->getNextSuggestion(titleC)) {
NSString *title = [NSString stringWithUTF8String:titleC.c_str()];
NSString *path = [self pageURLFromTitle:title];
NSNumber *distance = [NSNumber numberWithInteger:[self levenshteinDistance:searchTerm andString:title.lowercaseString]];
[results addObject:@{@"title": title,
@"path": path,
@"bookID": bookID,
@"distance": distance}];
@"bookID": bookID}];
}
}
return results;
@ -99,13 +97,11 @@
NSString *path = [NSString stringWithUTF8String:doc.get_data().c_str()];
NSString *title = [NSString stringWithUTF8String:doc.get_value(0).c_str()];
NSString *snippet = [NSString stringWithUTF8String:doc.get_value(1).c_str()];
NSNumber *distance = [NSNumber numberWithInteger:[self levenshteinDistance:searchTerm andString:title.lowercaseString]];
NSDictionary *result = @{@"title": title,
@"path": path,
@"bookID": bookID,
@"probability": percent,
@"distance": distance,
@"snippet": snippet};
[results addObject:result];
}
@ -343,4 +339,10 @@ int levenshtein_distance(const std::string &s1, const std::string &s2)
}
}
+ (NSInteger)levenshtein:(NSString *)strA anotherString:(NSString *)strB {
const string str1 = [strA cStringUsingEncoding:NSUTF8StringEncoding];
const string str2 = [strB cStringUsingEncoding:NSUTF8StringEncoding];
return levenshtein_distance(str1, str2);
}
@end