搜索關鍵字高亮顯示,就比微信多個多音字搜索

首先看下demo效果,下載地址git

一. 需求要求實現的效果

  • 漢字支持漢字直接搜索、拼音全拼搜索、拼音簡拼搜索
  • 搜索匹配到的關鍵字高亮顯示
  • 搜索結果優先顯示所有匹配、其次是拼音全拼匹配、拼音簡拼匹配;關鍵字在結果字符串中位置越靠前,優先顯示
  • 支持搜索英文、漢字、電話號碼及混合搜索

二. 需求分析

  • 英文名稱及電話號碼的搜索直接使用徹底匹配的方式便可
  • 重難點是漢字的拼音相關的拼音全拼、簡拼搜索,好比 「劉亦菲」 對應的搜索關鍵字有且只有如下三大類總計 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<name.length; x++) {
        NSRange range = NSMakeRange(x, 1);
        // 獲取字符
        NSString* hanyuCharString = [name substringWithRange:range];
        
        // 若是該字符是中文
        if ([WPFPinYinTools isChinese:hanyuCharString]) {
            // 獲取該字符的第一個拼音字母,如 wang 的 firstLetter 就是 w
            NSString *firstLetter = [WPFPinYinTools firstCharactor:hanyuCharString withFormat:pinyinFormat];
            // 獲取該字符的拼音全拼,如 王 的 pinyinString就是 wang
            NSString *pinyinString = [PinyinHelper toHanyuPinyinStringWithNSString:hanyuCharString withHanyuPinyinOutputFormat:pinyinFormat withNSString:@""];
            /** 👉 👉獲取該字符的拼音在整個字符串中的位置,如 "wang peng fei" 👈 👈 * 👉 👉 "wang" 對應的四個拼音字母是 0,0,0,0, 👈 👈 * 👉 👉 "peng" 對應的四個拼音字母是 1,1,1,1, 👈 👈 * 👉 👉 "fei" 對應的三個拼音字母是 2,2,2, 👈 👈 */
            for (NSInteger j= 0 ;j<pinyinString.length ; j++) {
                [completeSpellingArray addObject:@(x)];
            }
            // 拼接首字母字符串,如 "王鵬飛" 對應的首字母字符串就是 "wpf"
            initialString = [initialString stringByAppendingString:firstLetter];
            // 👉 👉 拼接首字母位置字符串,如 "王鵬飛" 對應的首字母位置就是 "0,1,2" 👈 👈
            [pinyinFirstLetterLocationArray addObject:@(x)];
            
        } else {
            [completeSpellingArray addObject:@(x)];
            [pinyinFirstLetterLocationArray addObject:@(x)];
            initialString = [initialString stringByAppendingString:hanyuCharString];
        }
    }
    person.name = name;
    person.completeSpelling = completeSpelling;
    person.initialString = initialString;
    person.pinyinLocationString = [completeSpellingArray componentsJoinedByString:@","];
    person.initialLocationString = [pinyinFirstLetterLocationArray componentsJoinedByString:@","];
    
    return person;
}
複製代碼
  • 根據 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 庫比較全,一些新的漢字也都能轉成拼音github

  • 可是因爲該庫很久沒有更新,獲取拼音文件部分代碼不適合組件化的直接開發,所以我直接合到源文件裏面了數組

  • 漢語轉拼音的格式緩存

// 獲取格式化器
+ (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 循環多線程

五. 爲何沒有選擇hash

  • 首先最重要的一條是當前循環的方式也能知足需求(線上大概四千多條數據,使用過程當中基本實時展示)
  • 上文在需求分析中已舉例,一個三個字的漢字對應的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 git@github.com:PengfeiWang666/HighlightedSearch.git
  • cd Example
  • open 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];
        }
}

複製代碼

最後再附一下demo地址app

相關文章
相關標籤/搜索