First look at the demo effect, download address

1. Requirements require the realization of the effect

  • Chinese characters support Chinese characters direct search, pinyin full search, pinyin simple search
  • Search keyword matches are highlighted
  • Search results give priority to display all matches, followed by pinyin full match, Pinyin Jane match; The higher the position of the keyword is in the result string, the keyword is displayed preferentially
  • Supports search in English, Chinese characters, phone numbers, and mixed search

2. Demand analysis

  • English names and phone numbers can be searched directly using a full match
  • The key and difficult points are the pinyin related to the whole pinyin, simple spelling search, such as “Liu Yifei” corresponding search keywordsOne and onlyThe following three categories total 25 matches
    • Chinese characters: “Liu”, “Yi”, “Fei”, “Liu Yifei”, “Liu Yifei”
    • Simple spelling related: “L”, “y”, “f”, “ly”, “yf”, “LYf”
    • All related: “li”, “liu”, “liuy”, “liuyif”, “liuyife”, “yi”, “yif”, “yife”, “yifei”, “fe”, “fei”
  • For example, if you search for the key word “xian”, you need to match both “Xian” and “Xi ‘an”

Iii. Code design

1. Overall process
  • First, initialize the original data (including Chinese, English, numbers and arbitrary combination), mainly converting a Chinese string intoChinese full spelling pinyin and each pinyin letter corresponding to the position of Chinese characters å’Œ Pinyin and the position of each pinyin letter corresponding to Chinese charactersTo cache the initialized information
+ (instancetype)personWithName:(NSString *)name hanyuPinyinOutputFormat:(HanyuPinyinOutputFormat *)pinyinFormat {
    WPFPerson *person = [[WPFPerson alloc] init];
    
    /** Class method for converting Chinese characters to pinyin * name: Chinese characters to be converted * pinyinFormat: Pinyin formatter * @"" : seperator separator */
    NSString *completeSpelling = [PinyinHelper toHanyuPinyinStringWithNSString:name withHanyuPinyinOutputFormat:pinyinFormat withNSString:@ ""];
    
    // A string consisting of the first letter
    NSString *initialString = @ "";
    // Full pinyin array
    NSMutableArray *completeSpellingArray = [[NSMutableArray alloc] init];
    // An array of alphabetic positions
    NSMutableArray *pinyinFirstLetterLocationArray = [[NSMutableArray alloc] init];
    
    // Iterate over each character
    for (NSInteger x =0; x<name.length; x++) {
        NSRange range = NSMakeRange(x, 1);
        // Get the character
        NSString* hanyuCharString = [name substringWithRange:range];
        
        // If the character is Chinese
        if ([WPFPinYinTools isChinese:hanyuCharString]) {
            // Get the first alphabetic letter of the character, as wang's firstLetter is w
            NSString *firstLetter = [WPFPinYinTools firstCharactor:hanyuCharString withFormat:pinyinFormat];
            // Get the full pinyin spelling of the character, such as Wang
            NSString *pinyinString = [PinyinHelper toHanyuPinyinStringWithNSString:hanyuCharString withHanyuPinyinOutputFormat:pinyinFormat withNSString:@ ""];
            /** 👉 👉 Such as "wang peng fei," 👈 👈 * 👉 👉 "wang," the four corresponding phonetic alphabet is 0,0,0,0, 👈 👈 * 👉 👉 "peng" corresponding to the four phonetic alphabet is 1,1,1,1, 👈 👈 * 👉 👉 the three pinyin letters for "fei" are 2,2,2, and 👈 👈 */
            for (NSInteger j= 0; j<pinyinString.length ; j++) { [completeSpellingArray addObject:@(x)]; }// Concatenate the initial character string, such as "Wang Pengfei" corresponding initial character string is "WPF"
            initialString = [initialString stringByAppendingString:firstLetter];
            / / 👉 👉 string splicing initials position, such as "wang" corresponding to the first letter of the position is "0" 👈 👈
            [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;
}
Copy the code
  • According to theUISearchResultsUpdatingProxy method- (void)updateSearchResultsForSearchController:(UISearchController *)searchControllerTo obtain the latest keyword input in real time, and traverse the data source, will match the results displayed
// Update the search results
- (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]; }};// Sort the matching results by product rules
    [self.searchResultVC.resultDataSource sortUsingDescriptors:[WPFPinYinTools sortingRules]];
    / / refresh tableView
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.searchResultVC.tableView reloadData];
    });
}
Copy the code
  • The matching process is a difficult point, respectively, Chinese characters matching, pinyin full spelling matching, pinyin simple spelling matching
+ (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:@ ","];
    
    // Full Chinese matching range
    NSRange chineseRange = [nameStrLower rangeOfString:searchStrLower];
    // Pinyin full matching range
    NSRange complateRange = [completeSpelling rangeOfString:searchStrLower];
    // Pinyin initials match range
    NSRange initialRange = [initialString rangeOfString:searchStrLower];
    
    // Chinese characters match directly
    if(chineseRange.length! =0) {
        searchModel.highlightedRange = chineseRange;
        searchModel.matchType = MatchTypeChinese;
        return searchModel;
    }
    
    NSRange highlightedRange = NSMakeRange(0.0);
    
    // MARK: All pinyin matches
    if(complateRange.length ! =0) {
        if (complateRange.location == 0) {
            // Pinyin matching starts from 0, i.e. the search keyword matches the first Character of the data source, so the highlighting range starts from 0
            highlightedRange = NSMakeRange(0, [completeSpellingArray[complateRange.length- 1] integerValue] +1);
            
        } else {
            /** If the pinyin character is the first character of a Chinese character, such as "G", * should match the pinyin characters starting with "G" such as "gai" and "ge", * should not match the pinyin characters starting with "g" such as "wang" and "feng" */
            NSInteger currentLocation = [completeSpellingArray[complateRange.location] integerValue];
            NSInteger lastLocation = [completeSpellingArray[complateRange.location- 1] integerValue];
            if(currentLocation ! = lastLocation) {// The highlight range starts with the first keyword matched
                highlightedRange = NSMakeRange(currentLocation, [completeSpellingArray[complateRange.length+complateRange.location - 1] integerValue] - currentLocation +1);
            }
        }
        searchModel.highlightedRange = highlightedRange;
        searchModel.matchType = MatchTypeComplate;
        if(highlightedRange.length! =0) {
            returnsearchModel; }}// MARK: Pinyin initials match
    if(initialRange.length! =0) {
        NSInteger currentLocation = [pinyinFirstLetterLocationArray[initialRange.location] integerValue];
        NSInteger highlightedLength;
        if (initialRange.location ==0) {
            highlightedLength = [pinyinFirstLetterLocationArray[initialRange.length- 1] integerValue]-currentLocation +1;
            // Pinyin matching starts from 0, i.e. the search keyword matches the first Character of the data source, so the highlighting range starts from 0
            highlightedRange = NSMakeRange(0, highlightedLength);
        } else {
            highlightedLength = [pinyinFirstLetterLocationArray[initialRange.length+initialRange.location- 1] integerValue]-currentLocation +1;
            // The highlight range starts with the first keyword matched
            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;
}
Copy the code
2. Third-party dependencies
  • First of all, a relatively complete third-party library PinYin4Objc is selected for Converting Chinese into Pinyin. Pinyin has a relatively complete Unicode library, and some new Chinese characters can also be converted into Pinyin

  • However, since the library has not been updated for a long time, obtaining the pinyin file part of the code is not suitable for the direct development of componentization, so I directly merged into the source file inside

  • Chinese to pinyin format

// Get the formatter
+ (HanyuPinyinOutputFormat *)getOutputFormat {
    HanyuPinyinOutputFormat *pinyinFormat = [[HanyuPinyinOutputFormat alloc] init];
    CaseTypeLowercase: lowercase * CaseTypeUppercase: uppercase */
    [pinyinFormat setCaseType:CaseTypeLowercase];
    Wang2 peng2 fei1 * ToneTypeWithoutTone: Silent tones indicate tones wang peng fei * ToneTypeWithToneMark: use characters to indicate tones wang peng fē I */
    [pinyinFormat setToneType:ToneTypeWithoutTone];
    /** Set the display format of the special pinyin u: * VCharTypeWithUAndColon: u and a colon, e.g. V indicates the character, for example, lv * VCharTypeWithUUnicode: indicates u */
    [pinyinFormat setVCharType:VCharTypeWithV];
    return pinyinFormat;
}
Copy the code
3. Other details
  • collation
+ (NSArray *)sortingRules {
    // According to matchType order, that is, Chinese is displayed first, followed by full spelling match, and finally pinyin initials match
    NSSortDescriptor *desType = [NSSortDescriptor sortDescriptorWithKey:@"matchType" ascending:YES];
    // Give priority to the search results at the top of the highlighted index
    NSSortDescriptor *desLocation = [NSSortDescriptor sortDescriptorWithKey:@"highlightLoaction" ascending:YES];
    return @[desType,desLocation];
}
Copy the code

#### iv. Cyclic method testing and optimizing selection process

In the process of optimizing the traversal method, several traversal methods were tested. Here, the keyword “Wang” was entered as the test data, and the test model was iPhone SE 10.3

  • Regular for loop
/** 2017-12-06 12:02:51.943006 HighlightedSearch[4459:1871193] w 2017-12-06 12:02:51.943431 HighlightedSearch[4459:1871193] starts matching at: 2017-12-06 04:02:51 +0000 2017-12-06 12:02:51.980588 HighlightedSearch[4459:1871193] 2017-12-06 04:02:51 +0000 284488 HighlightedSearch[4459:1871193] wa 2017-12-06 12:02:52.284771 HighlightedSearch[4459:1871193] starts matching at: 2017-12-06 04:02:52 +0000 2017-12-06 12:02:52.316536 HighlightedSearch[4459:1871193] 2017-12-06 04:02:52 +0000 time: HighlightedSearch[4459:1871193] WAN 2017-12-06 12:02:52.517121 HighlightedSearch[4459:1871193] starts matching at: 2017-12-06 04:02:52 +0000 2017-12-06 12:02:52.545542 HighlightedSearch[4459:1871193] 2017-12-06 04:02:52 +0000 time: [4459:1871193] Wang 2017-12-06 12:02:52.838602 HighlightedSearch[4459:1871193] starts matching at: 2017-12-06 04:02:52 +0000 2017-12-06 12:02:52.880200 HighlightedSearch[4459:1871193] 2017-12-06 04:02:52 +0000, time: 0.0417 */
for (NSInteger i = 0; i < self.dataSource.count; i++) {
Copy the code
  • GCD multithreaded loop
/** 2017-12-06 11:56:55.565738 HighlightedSearch[4419:1869486] W 2017-12-06 11:56:55.566287 HighlightedSearch[4419:1869486] starts matching at: 2017-12-06 03:56:55 +0000 2017-12-06 11:56:55.626184 HighlightedSearch[4419:1869486] 2017-12-06 03:56:55 +0000 time: 0.0601 2017-12-06 11:56:55.937535 HighlightedSearch[4419:1869486] WA 2017-12-06 11:56:55.937842 HighlightedSearch[4419:1869486] starts matching at: 2017-12-06 03:56:55 +0000 2017-12-06 11:56:55.983074 HighlightedSearch[4419:1869486] 2017-12-06 03:56:55 +0000 time: HighlightedSearch[4419:1869486] WAN 2017-12-06 11:56:56.347350 HighlightedSearch[4419:1869486] starts matching at: 2017-12-06 03:56:56 +0000 2017-12-06 11:56:56.414215 HighlightedSearch[4419:1869486] 2017-12-06 03:56:56 +0000 time: 711174 HighlightedSearch[4419:1869486] Wang 2017-12-06 11:56:56.712013 HighlightedSearch[4419:1869486] starts matching at: 2017-12-06 03:56:56 +0000 2017-12-06 11:56:56.774761 HighlightedSearch[4419:1869486] 2017-12-06 03:56:56 +0000, Time: 0.0632 */
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(self.dataSource.count, queue, ^(size_t index) {
Copy the code
  • EnumerateObjectsWithOptions multithreaded cycle
/** 2017-12-06 11:58:12.716606 HighlightedSearch[4428:1869917] w 2017-12-06 11:58:12.717005 HighlightedSearch[4428:1869917] starts matching at: 2017-12-06 03:58:12 +0000 2017-12-06 11:58:12.780168 HighlightedSearch[4428:1869917] 2017-12-06 03:58:12 +0000, Time: 058590 HighlightedSearch[4428:1869917] wa 2017-12-06 11:58:13.058841 HighlightedSearch[4428:1869917] starts matching at: 2017-12-06 03:58:13 +0000 2017-12-06 11:58:13.116964 HighlightedSearch[4428:1869917] 2017-12-06 03:58:13 +0000, Time: HighlightedSearch[4428:1869917] WAN 2017-12-06 11:58:13.397338 HighlightedSearch[4428:1869917] starts matching at: 2017-12-06 03:58:13 +0000 2017-12-06 11:58:13.460298 HighlightedSearch[4428:1869917] 2017-12-06 03:58:13 +0000, Time: Search[4428:1869917] Wang 2017-12-06 11:58:13.764263 HighlightedSearch[4428:1869917] starts matching at: 2017-12-06 03:58:13 +0000 2017-12-06 11:58:13.833888 HighlightedSearch[4428:1869917] 2017-12-06 03:58:13 +0000, time: 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) {
Copy the code
  • Forin cycle
/** 2017-12-06 12:00:38.217187 HighlightedSearch[4439.1870645] w 2017-12-06 12:00:38.217575 HighlightedSearch[4439:1870645] start match, start time: 2017-12-06 04:00:38 +0000 2017-12-06 12:00:38.253997 HighlightedSearch[4439:1870645] 2017-12-06 04:00:38 +0000 time: 0.0364 2017-12-06 12:00:38.616430 HighlightedSearch[4439-1870645] WA 2017-12-06 12:00:38.616807 HighlightedSearch[4439:1870645] start match, start time: 2017-12-06 04:00:38 +0000 2017-12-06 12:00:38.654969 HighlightedSearch[4439:1870645] 2017-12-06 04:00:38 +0000 time: HighlightedSearch[4439:1870645] WAN 2017-12-06 12:00:38.949453 HighlightedSearch[4439:1870645] start match, start time: 2017-12-06 04:00:38 +0000 2017-12-06 12:00:38.986892 HighlightedSearch[4439:1870645] 2017-12-06 04:00:38 +0000 time: [2017-12-06 12:00:39.280979] Wang 2017-12-06 12:00:39.281563 HighlightedSearch[4439:1870645] start match, start time: 2017-12-06 04:00:39 +0000 2017-12-06 12:00:39.317743 HighlightedSearch[4439:1870645] 2017-12-06 04:00:39 +0000, Time: 0.0365 */
for (WPFPerson *person in self.dataSource) {
Copy the code

Final choice is forin cycle, because usually enumerateObjectsWithOptions multithreading is the fastest, and a little faster than dispatch_apply method, but because this method need to manipulate arrays, Therefore, the line of code that manipulates the data must be locked or executed on a specified thread, which is less efficient than other single-threaded loops, and the Forin loop was chosen because the search results would have to be sorted by rules again

Why is hash not selected

  • The first and most important one is that the current way of circulation can also meet the demand (there are about 4,000 data online, which are basically displayed in real time during use).
  • As mentioned in the demand analysis above, there are more than 20 or more key values corresponding to a three-character Chinese character, which is very time-consuming in the process of analysis. However, there are often “group name” matching similar to wechat, and each additional word corresponds to several orders of magnitude more key values
  • In the case of high concurrency, mapTables need to continuously Resize (expand & Rehash), and may form linked list rings in the case of concurrent Rehash
  • There is an optimization idea, considering the way of traversal parsing fast, search and match slow; The hash method is slow in parsing and fast in searching and matching
    • The data is quickly parsed by traversal. In this case, traversal is used for search
    • (Considering the instantaneous efficiency of expanding hash table, to avoid frequent expansion, use bucket sorting to place 10 numbers, 26 Letters, and keys starting with special symbols in 37 dictionaries, forming an array. Each dictionary stores the corresponding key and value). After parsing, make a mark and use the hash method to directly query the input key value
    • With DB caching, the effect should be great

Six. Polyphonic characters

A quick test of products with this feature:

  • Wechat search (the type of search described in this article) is done locally and does not support polyphonics
  • Nail nail search is done by the server, support polyphonic characters (but a simple test of some basic polyphonic characters there are bugs)

7. What else should be done for the actual project?

  • Under normal circumstances, all the matching results will not be displayed in the first time. Generally, three or five results are displayed for product requirements. Therefore, the cycle can be stopped after matching several results, and click more to match the remaining data sources
  • With DB and hashTable, new data sources are parsed at a time and cached after being parsed

Eight. Use method

1. Case project
  • git clone [email protected]:PengfeiWang666/HighlightedSearch.git
  • cd Example
  • open HighlightedSearch.xcworkspace
2. Install
  • pod “HighlightedSearch”
3. Usage
// WPFPinYinDataManager adds data sources in turn (identifiers to prevent duplicate names)
+ (void)addInitializeString:(NSString *)string identifer:(NSString *)identifier

// Update the search results
- (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]; }}Copy the code

Finally, attach the demo address