漢字支持漢字直接搜索、拼音全拼搜索、拼音簡拼搜索
搜索匹配到的關鍵字高亮顯示
搜索結果優先顯示全部匹配、其次是拼音全拼匹配、拼音簡拼匹配;關鍵字在結果字符串中位置越靠前,優先顯示
支持搜索英文、漢字、電話號碼及混合搜索
英文名稱及電話號碼的搜索直接使用完全匹配的方式即可
重難點是漢字的拼音相關的拼音全拼、簡拼搜索,比如 “劉亦菲” 對應的搜索關鍵字有且只有以下三大類總計 25 種匹配漢字:“劉”、“亦”、“菲”、“劉亦”、“亦菲”、“劉亦菲”
簡拼相關:"l"、"y"、"f"、"ly"、"yf"、"lyf"
全拼相關:"li"、"liu"、"liuy"、"liuyi"、"liuyif"、"liuyife"、"liuyifei"、"yi"、"yif"、"yife"、"yifei"、"fe"、"fei"
拼音的重難點還包括:比如搜索關鍵字為“xian”,既要匹配出“先”,也要匹配出“西安”
1. 整體流程
首先初始化原始的數據(包含漢語、英文、數字及隨意組合),主要是將一個漢語字符串轉化為漢語全拼拼音及每個拼音字母所對應漢字的位置 和 漢語簡拼拼音和每個拼音字母對應漢字的位置,將初始化之后的信息緩存起來
+ (instancetype)personWithName:(NSString *)name hanyuPinyinOutputFormat:(HanyuPinyinOutputFormat *)pinyinFormat { WPFPerson *person = [[WPFPerson alloc] init]; /** 將漢字轉化為拼音的類方法 * name : 需要轉換的漢字 * pinyinFormat : 拼音的格式化器 * @"" : seperator 分隔符 */ NSString *completeSpelling = [PinyinHelper toHanyuPinyinStringWithNSString:name withHanyuPinyinOutputFormat:pinyinFormat withNSString:@""]; // 首字母所組成的字符串 NSString *initialString = @""; // 全拼拼音數組 NSMutableArray *completeSpellingArray = [[NSMutableArray alloc] init]; // 拼音首字母的位置數組 NSMutableArray *pinyinFirstLetterLocationArray = [[NSMutableArray alloc] init]; // 遍歷每一個字符 for (NSInteger x =0; x根據 UISearchResultsUpdating 代理方法 - (void)updateSearchResultsForSearchController:(UISearchController *)searchController 來實時獲取輸入的最新關鍵字,并遍歷數據源,將匹配到的結果顯示出來// 更新搜索結果- (void)updateSearchResultsForSearchController:(UISearchController *)searchController { NSLog(@"%@", searchController.searchBar.text); [self.searchResultVC.resultDataSource removeAllObjects]; for (WPFPerson *person in self.dataSource) { WPFSearchResultModel *resultModel = [WPFPinYinTools searchEffectiveResultWithSearchString:searchController.searchBar.text.lowercaseString nameString:person.name completeSpelling:person.completeSpelling initialString:person.initialString pinyinLocationString:person.pinyinLocationString initialLocationString:person.initialLocationString]; if (resultModel.highlightRang.length) { person.highlightLoaction = resultModel.highlightRang.location; person.textRange = resultModel.highlightRang; person.matchType = resultModel.matchType; [self.searchResultVC.resultDataSource addObject:person]; } }; // 將匹配結果按照產品規則進行排序 [self.searchResultVC.resultDataSource sortUsingDescriptors:[WPFPinYinTools sortingRules]]; // 刷新tableView dispatch_async(dispatch_get_main_queue(), ^{ [self.searchResultVC.tableView reloadData]; });}
匹配的過程是一個重難點,分別進行漢字直接匹配、拼音全拼匹配、拼音簡拼匹配
+ (WPFSearchResultModel *)searchEffectiveResultWithSearchString:(NSString *)searchStrLower nameString:(NSString *)nameStrLower completeSpelling:(NSString *)completeSpelling initialString:(NSString *)initialString pinyinLocationString:(NSString *)pinyinLocationString initialLocationString:(NSString *)initialLocationString { WPFSearchResultModel *searchModel = [[WPFSearchResultModel alloc] init]; NSArray *completeSpellingArray = [pinyinLocationString componentsSeparatedByString:@","]; NSArray *pinyinFirstLetterLocationArray = [initialLocationString componentsSeparatedByString:@","]; // 完全中文匹配范圍 NSRange chineseRange = [nameStrLower rangeOfString:searchStrLower]; // 拼音全拼匹配范圍 NSRange complateRange = [completeSpelling rangeOfString:searchStrLower]; // 拼音首字母匹配范圍 NSRange initialRange = [initialString rangeOfString:searchStrLower]; // 漢字直接匹配 if (chineseRange.length!=0) { searchModel.highlightedRange = chineseRange; searchModel.matchType = MatchTypeChinese; return searchModel; } NSRange highlightedRange = NSMakeRange(0, 0); // MARK: 拼音全拼匹配 if (complateRange.length != 0) { if (complateRange.location == 0) { // 拼音首字母匹配從0開始,即搜索的關鍵字與該數據源第一個漢字匹配到,所以高亮范圍從0開始 highlightedRange = NSMakeRange(0, [completeSpellingArray[complateRange.length-1] integerValue] +1); } else { /** 如果該拼音字符是一個漢字的首個字符,如搜索“g”, * 就要匹配出“gai”、“ge”等“g”開頭的拼音對應的字符, * 而不應該匹配到“wang”、“feng”等非”g“開頭的拼音對應的字符 */ NSInteger currentLocation = [completeSpellingArray[complateRange.location] integerValue]; NSInteger lastLocation = [completeSpellingArray[complateRange.location-1] integerValue]; if (currentLocation != lastLocation) { // 高亮范圍從匹配到的第一個關鍵字開始 highlightedRange = NSMakeRange(currentLocation, [completeSpellingArray[complateRange.length+complateRange.location -1] integerValue] - currentLocation +1); } } searchModel.highlightedRange = highlightedRange; searchModel.matchType = MatchTypeComplate; if (highlightedRange.length!=0) { return searchModel; } } // MARK: 拼音首字母匹配 if (initialRange.length!=0) { NSInteger currentLocation = [pinyinFirstLetterLocationArray[initialRange.location] integerValue]; NSInteger highlightedLength; if (initialRange.location ==0) { highlightedLength = [pinyinFirstLetterLocationArray[initialRange.length-1] integerValue]-currentLocation +1; // 拼音首字母匹配從0開始,即搜索的關鍵字與該數據源第一個漢字匹配到,所以高亮范圍從0開始 highlightedRange = NSMakeRange(0, highlightedLength); } else { highlightedLength = [pinyinFirstLetterLocationArray[initialRange.length+initialRange.location-1] integerValue]-currentLocation +1; // 高亮范圍從匹配到的第一個關鍵字開始 highlightedRange = NSMakeRange(currentLocation, highlightedLength); } searchModel.highlightedRange = highlightedRange; searchModel.matchType = MatchTypeInitial; if (highlightedRange.length!=0) { return searchModel; } } searchModel.highlightedRange = NSMakeRange(0, 0); searchModel.matchType = NSIntegerMax; return searchModel;}
2. 第三方依賴
首先篩選出一個比較全的第三方庫 PinYin4Objc用于漢語轉拼音,拼音的 unicode 庫比較全,一些新的漢字也都能轉成拼音
但是由于該庫好久沒有更新,獲取拼音文件部分代碼不適合組件化的直接開發,因此我直接合到源文件里面了
漢語轉拼音的格式
// 獲取格式化器+ (HanyuPinyinOutputFormat *)getOutputFormat { HanyuPinyinOutputFormat *pinyinFormat = [[HanyuPinyinOutputFormat alloc] init]; /** 設置大小寫 * CaseTypeLowercase : 小寫 * CaseTypeUppercase : 大寫 */ [pinyinFormat setCaseType:CaseTypeLowercase]; /** 聲調格式 :如 王鵬飛 * ToneTypeWithToneNumber : 用數字表示聲調 wang2 peng2 fei1 * ToneTypeWithoutTone : 無聲調表示 wang peng fei * ToneTypeWithToneMark : 用字符表示聲調 wáng péng fēi */ [pinyinFormat setToneType:ToneTypeWithoutTone]; /** 設置特殊拼音ü的顯示格式: * VCharTypeWithUAndColon : 以U和一個冒號表示該拼音,例如:lu: * VCharTypeWithV : 以V表示該字符,例如:lv * VCharTypeWithUUnicode : 以ü表示 */ [pinyinFormat setVCharType:VCharTypeWithV]; return pinyinFormat;}
3. 其他細節
排序規則
+ (NSArray *)sortingRules { // 按照 matchType 順序排列,即優先展示 中文,其次是全拼匹配,最后是拼音首字母匹配 NSSortDescriptor *desType = [NSSortDescriptor sortDescriptorWithKey:@"matchType" ascending:YES]; // 優先顯示 高亮位置索引靠前的搜索結果 NSSortDescriptor *desLocation = [NSSortDescriptor sortDescriptorWithKey:@"highlightLoaction" ascending:YES]; return @[desType,desLocation];}
在優化遍歷方法的過程中,測試了幾種遍歷方法,這里以輸入關鍵字“wang”為測試數據,測試真機機型為iPhone SE 10.3
常規 for 循環
/** 2017-12-06 12:02:51.943006 HighlightedSearch[4459:1871193] w 2017-12-06 12:02:51.943431 HighlightedSearch[4459:1871193] 開始匹配,開始時間:2017-12-06 04:02:51 +0000 2017-12-06 12:02:51.980588 HighlightedSearch[4459:1871193] 匹配結束,結束時間:2017-12-06 04:02:51 +0000,耗時:0.0372 2017-12-06 12:02:52.284488 HighlightedSearch[4459:1871193] wa 2017-12-06 12:02:52.284771 HighlightedSearch[4459:1871193] 開始匹配,開始時間:2017-12-06 04:02:52 +0000 2017-12-06 12:02:52.316536 HighlightedSearch[4459:1871193] 匹配結束,結束時間:2017-12-06 04:02:52 +0000,耗時:0.0318 2017-12-06 12:02:52.516826 HighlightedSearch[4459:1871193] wan 2017-12-06 12:02:52.517121 HighlightedSearch[4459:1871193] 開始匹配,開始時間:2017-12-06 04:02:52 +0000 2017-12-06 12:02:52.545542 HighlightedSearch[4459:1871193] 匹配結束,結束時間:2017-12-06 04:02:52 +0000,耗時:0.0285 2017-12-06 12:02:52.838220 HighlightedSearch[4459:1871193] wang 2017-12-06 12:02:52.838602 HighlightedSearch[4459:1871193] 開始匹配,開始時間:2017-12-06 04:02:52 +0000 2017-12-06 12:02:52.880200 HighlightedSearch[4459:1871193] 匹配結束,結束時間:2017-12-06 04:02:52 +0000,耗時:0.0417 */for (NSInteger i = 0; i < self.dataSource.count; i++) {
GCD 多線程循環
/** 2017-12-06 11:56:55.565738 HighlightedSearch[4419:1869486] w 2017-12-06 11:56:55.566287 HighlightedSearch[4419:1869486] 開始匹配,開始時間:2017-12-06 03:56:55 +0000 2017-12-06 11:56:55.626184 HighlightedSearch[4419:1869486] 匹配結束,結束時間:2017-12-06 03:56:55 +0000,耗時:0.0601 2017-12-06 11:56:55.937535 HighlightedSearch[4419:1869486] wa 2017-12-06 11:56:55.937842 HighlightedSearch[4419:1869486] 開始匹配,開始時間:2017-12-06 03:56:55 +0000 2017-12-06 11:56:55.983074 HighlightedSearch[4419:1869486] 匹配結束,結束時間:2017-12-06 03:56:55 +0000,耗時:0.0452 2017-12-06 11:56:56.344808 HighlightedSearch[4419:1869486] wan 2017-12-06 11:56:56.347350 HighlightedSearch[4419:1869486] 開始匹配,開始時間:2017-12-06 03:56:56 +0000 2017-12-06 11:56:56.414215 HighlightedSearch[4419:1869486] 匹配結束,結束時間:2017-12-06 03:56:56 +0000,耗時:0.0690 2017-12-06 11:56:56.711174 HighlightedSearch[4419:1869486] wang 2017-12-06 11:56:56.712013 HighlightedSearch[4419:1869486] 開始匹配,開始時間:2017-12-06 03:56:56 +0000 2017-12-06 11:56:56.774761 HighlightedSearch[4419:1869486] 匹配結束,結束時間:2017-12-06 03:56:56 +0000,耗時:0.0632 */dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);dispatch_apply(self.dataSource.count, queue, ^(size_t index) {
enumerateObjectsWithOptions 多線程循環
/** 2017-12-06 11:58:12.716606 HighlightedSearch[4428:1869917] w 2017-12-06 11:58:12.717005 HighlightedSearch[4428:1869917] 開始匹配,開始時間:2017-12-06 03:58:12 +0000 2017-12-06 11:58:12.780168 HighlightedSearch[4428:1869917] 匹配結束,結束時間:2017-12-06 03:58:12 +0000,耗時:0.0633 2017-12-06 11:58:13.058590 HighlightedSearch[4428:1869917] wa 2017-12-06 11:58:13.058841 HighlightedSearch[4428:1869917] 開始匹配,開始時間:2017-12-06 03:58:13 +0000 2017-12-06 11:58:13.116964 HighlightedSearch[4428:1869917] 匹配結束,結束時間:2017-12-06 03:58:13 +0000,耗時:0.0581 2017-12-06 11:58:13.397052 HighlightedSearch[4428:1869917] wan 2017-12-06 11:58:13.397338 HighlightedSearch[4428:1869917] 開始匹配,開始時間:2017-12-06 03:58:13 +0000 2017-12-06 11:58:13.460298 HighlightedSearch[4428:1869917] 匹配結束,結束時間:2017-12-06 03:58:13 +0000,耗時:0.0630 2017-12-06 11:58:13.763888 HighlightedSearch[4428:1869917] wang 2017-12-06 11:58:13.764263 HighlightedSearch[4428:1869917] 開始匹配,開始時間:2017-12-06 03:58:13 +0000 2017-12-06 11:58:13.833888 HighlightedSearch[4428:1869917] 匹配結束,結束時間:2017-12-06 03:58:13 +0000,耗時:0.0697 */dispatch_queue_t queue = dispatch_queue_create("wpf.updateSearchResults.test", DISPATCH_QUEUE_SERIAL);[self.dataSource enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
forin 循環
/** 2017-12-06 12:00:38.217187 HighlightedSearch[4439:1870645] w 2017-12-06 12:00:38.217575 HighlightedSearch[4439:1870645] 開始匹配,開始時間:2017-12-06 04:00:38 +0000 2017-12-06 12:00:38.253997 HighlightedSearch[4439:1870645] 匹配結束,結束時間:2017-12-06 04:00:38 +0000,耗時:0.0364 2017-12-06 12:00:38.616430 HighlightedSearch[4439:1870645] wa 2017-12-06 12:00:38.616807 HighlightedSearch[4439:1870645] 開始匹配,開始時間:2017-12-06 04:00:38 +0000 2017-12-06 12:00:38.654969 HighlightedSearch[4439:1870645] 匹配結束,結束時間:2017-12-06 04:00:38 +0000,耗時:0.0383 2017-12-06 12:00:38.948700 HighlightedSearch[4439:1870645] wan 2017-12-06 12:00:38.949453 HighlightedSearch[4439:1870645] 開始匹配,開始時間:2017-12-06 04:00:38 +0000 2017-12-06 12:00:38.986892 HighlightedSearch[4439:1870645] 匹配結束,結束時間:2017-12-06 04:00:38 +0000,耗時:0.0378 2017-12-06 12:00:39.280979 HighlightedSearch[4439:1870645] wang 2017-12-06 12:00:39.281563 HighlightedSearch[4439:1870645] 開始匹配,開始時間:2017-12-06 04:00:39 +0000 2017-12-06 12:00:39.317743 HighlightedSearch[4439:1870645] 匹配結束,結束時間:2017-12-06 04:00:39 +0000,耗時:0.0365 */for (WPFPerson *person in self.dataSource) {
最終選擇的是forin循環,因為一般情況下 enumerateObjectsWithOptions 多線程是最快的,并且稍快于 dispatch_apply 方法,但是因為這個方法需要操作數組,因此必須將操作數據的那行代碼加鎖或者在指定線程進行,進行這個操作后效率反而不如其他單線程循環,考慮到搜索結果本來還要再次根據規則排序,就選擇了 forin 循環
首先最重要的一條是當前循環的方式也能滿足需求(線上大概四千多條數據,使用過程中基本實時展現)
上文在需求分析中已舉例,一個三個字的漢字對應的key值就有20多個甚至更多,在解析過程中是十分耗時的,但需求往往還存在類似微信的“群名稱”匹配,每多一個字,對應的key值就多幾個數量級
MapTable在高并發情況下,需要不斷進行Resize(擴容 & Rehash),并且在Rehash 并發的情況下還可能形成鏈表環有個優化的思路,考慮到遍歷的方式解析快,搜索匹配慢;hash的方式解析慢,搜索匹配快
通過遍歷的方式先快速解析數據,此時搜索使用遍歷的方式
然后再用hash的方式再次解析數據(考慮到hash表的擴容會使得瞬時效率的降低,為了避免頻繁的擴容,先使用桶排序的方法將10個數字、26個英文字母、以及特殊符號開頭的key分別放在37個字典里面,整體是一個數組。每個字典里面存放對應key和value),解析完成之后做個標記就采用hash的方式直接使用輸入的key值去查詢
配合DB緩存,效果應該是很棒的
簡單測了一下擁有該功能的產品:
微信搜索(就是文中講的該類型搜索)是在本地做的,不支持多音字
釘釘的搜索是服務器做的,支持多音字(但是簡單測了一下一些基本的多音字存在bug)
正常情況下不會將所有的匹配結果在第一時間全部顯示,一般產品需求顯示三五個即可,因此可以匹配出若干個結果后停止循環,點擊更多再匹配剩余數據源
配合DB和hashTable,每次只解析新增的數據源,解析一次后就緩存起來
1. 事例工程
git clone [email protected]:PengfeiWang666/HighlightedSearch.gitcd Exampleopen HighlightedSearch.xcworkspace
2. Install
pod "HighlightedSearch"
3. Usage
// WPFPinYinDataManager 依次添加數據源(標識符為了防止重名現象)+ (void)addInitializeString:(NSString *)string identifer:(NSString *)identifier// 更新搜索結果- (void)updateSearchResultsForSearchController:(UISearchController *)searchController { ... ... for (WPFPerson *person in [WPFPinYinDataManager getInitializedDataSource]) { WPFSearchResultModel *resultModel = [WPFPinYinTools searchEffectiveResultWithSearchString:keyWord Person:person]; if (resultModel.highlightedRange.length) { person.highlightLoaction = resultModel.highlightedRange.location; person.textRange = resultModel.highlightedRange; person.matchType = resultModel.matchType; [resultDataSource addObject:person]; }}
最后附上源碼:https://github.com/PengfeiWang666/HighlightedSearch
|
新聞熱點
疑難解答