首先看下demo效果,下载地址
一. 需求要求实现的效果
- 汉字支持汉字直接搜索、拼音全拼搜索、拼音简拼搜索
- 搜索匹配到的关键字高亮显示
- 搜索结果优先显示全部匹配、其次是拼音全拼匹配、拼音简拼匹配;关键字在结果字符串中位置越靠前,优先显示
- 支持搜索英文、汉字、电话号码及混合搜索
二. 需求分析
- 英文名称及电话号码的搜索直接使用完全匹配的方式即可
- 重难点是汉字的拼音相关的拼音全拼、简拼搜索,比如 “刘亦菲” 对应的搜索关键字
有且只有
以下三大类总计 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];
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]) {
NSString *firstLetter = [WPFPinYinTools firstCharactor:hanyuCharString withFormat:pinyinFormat];
NSString *pinyinString = [PinyinHelper toHanyuPinyinStringWithNSString:hanyuCharString withHanyuPinyinOutputFormat:pinyinFormat withNSString:@""];
for (NSInteger j= 0 ;j<pinyinString.length ; j++) {
[completeSpellingArray addObject:@(x)];
}
initialString = [initialString stringByAppendingString:firstLetter];
[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]];
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);
if (complateRange.length != 0) {
if (complateRange.location == 0) {
highlightedRange = NSMakeRange(0, [completeSpellingArray[complateRange.length-1] integerValue] +1);
} else {
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;
}
}
if (initialRange.length!=0) {
NSInteger currentLocation = [pinyinFirstLetterLocationArray[initialRange.location] integerValue];
NSInteger highlightedLength;
if (initialRange.location ==0) {
highlightedLength = [pinyinFirstLetterLocationArray[initialRange.length-1] integerValue]-currentLocation +1;
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. 第三方依赖
+ (HanyuPinyinOutputFormat *)getOutputFormat {
HanyuPinyinOutputFormat *pinyinFormat = [[HanyuPinyinOutputFormat alloc] init];
[pinyinFormat setCaseType:CaseTypeLowercase];
[pinyinFormat setToneType:ToneTypeWithoutTone];
[pinyinFormat setVCharType:VCharTypeWithV];
return pinyinFormat;
}
3. 其他细节
+ (NSArray *)sortingRules {
NSSortDescriptor *desType = [NSSortDescriptor sortDescriptorWithKey:@"matchType" ascending:YES];
NSSortDescriptor *desLocation = [NSSortDescriptor sortDescriptorWithKey:@"highlightLoaction" ascending:YES];
return @[desType,desLocation];
}
####四. 循环方法测试及优化选择过程
在优化遍历方法的过程中,测试了几种遍历方法,这里以输入关键字“wang”为测试数据,测试真机机型为iPhone SE 10.3
for (NSInteger i = 0; i < self.dataSource.count; i++) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(self.dataSource.count, queue, ^(size_t index) {
- enumerateObjectsWithOptions 多线程循环
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) {
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
3. Usage
+ (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地址