在处理百万量级条目(如文本名)的搜索时,每一次匹配的效率对提高总搜索时间至关重要。如果在每次检查文件名与关键字时执行复杂的操作,会对总时间产生累计影响,进而影响用户体验。本文将详细分享之前 TDS 的文本搜索逻辑,希望能为大家提供一些参考。

一、拼音首字母转换

考虑字符串“123四五六78abc”,我们的预期是匹配关键字[“sw”,“六”]时能分别命中子串[“四五”,“六”]。要获取汉字的拼音首字母,需要先获取字符的 Unicode 编码,在不考虑多音字的情况下,可直接通过查表简单实现。如果字符处于中文区间,可直接返回对应的首字母。

从零开始:C# 文件名搜索拼音首字母支持、搜索加速策略与关键词高亮显示

在处理百万量级条目(如文本名)的搜索时,每一次匹配的效率对提高总搜索时间至关重要。如果在每次检查文件名与关键字时执行复杂的操作,会对总时间产生累计影响,进而影响用户体验。本文将详细分享之前 TDS 的文本搜索逻辑,希望能为大家提供一些参考。

一、拼音首字母转换

考虑字符串“123四五六78abc”,我们的预期是匹配关键字[“sw”,“六”]时能分别命中子串[“四五”,“六”]。要获取汉字的拼音首字母,需要先获取字符的 Unicode 编码,在不考虑多音字的情况下,可直接通过查表实现。如果字符处于中文区间,可直接返回对应的首字母。具体实现如下:

return iCnChar switch
{
>= 45217 and <= 45252 => 'A',
>= 45253 and <= 45760 => 'B',
>= 45761 and <= 46317 => 'C',
>= 46318 and <= 46825 => 'D',
>= 46826 and <= 47009 => 'E',
>= 47010 and <= 47296 => 'F',
>= 47297 and <= 47613 => 'G',
>= 47614 and <= 48118 => 'H',
>= 48119 and <= 49061 => 'J',
>= 49062 and <= 49323 => 'K',
>= 49324 and <= 49895 => 'L',
>= 49896 and <= 50370 => 'M',
>= 50371 and <= 50613 => 'N',
>= 50614 and <= 50621 => 'O',
>= 50622 and <= 50905 => 'P',
>= 50906 and <= 51386 => 'Q',
>= 51387 and <= 51445 => 'R',
>= 51446 and <= 52217 => 'S',
>= 52218 and <= 52697 => 'T',
>= 52698 and <= 52979 => 'W',
>= 52980 and <= 53688 => 'X',
>= 53689 and <= 54480 => 'Y',
>= 54481 and <= 65289 => 'Z',
_ => throw new ArgumentOutOfRangeException(nameof(iCnChar), iCnChar, null)
};

经过上述转换,字符串“123四五六78abc”将得到一个新的字符串“123swl78abc”。因此,对源文本的搜索需要对原字符串和拼音串进行两次匹配。目前尝试了多个方法,发现String.Contains的效率最高。StringComparison.OrdinalIgnoreCase 参数是一种快速的字符串比较方式,它忽略大小写差异,直接比较字符的 Unicode 值,避免了额外的字符转换操作。使用内置方法可以充分利用底层优化,减少不必要的计算,从而提高整体性能。

二、存储及索引辅助优化

拼音首字母预先转换比实时拼音转换匹配速度要高效得多,除非存储空间实在紧张。但多了一个字符串,也意味着多了一次字符串搜索,而字符串搜索是一个较占时间的操作。有没有办法继续优化呢?

(一)模式串

首先建立一个特定长度的char数组作为模式串,以长度为 6 的[“a”,”b”,”c”,”1”,”2”,”3”]为例,这个模式串包含了我们关心的元素。由于我们所有的中文全部映射到了字母的空间,因此这个模式串其实是有限的. 0-9共10个符号加上26个字母以及其他标点一共也不到64个,所以我们可以很方便的用一个64位长度'long'类型存储编码.

(二)字符串编码

以目标文件名“apple 1”为例,我们初始化一个对应模式串长度的二进制位,0_0_0_0_0_0,逐字符扫描时,发现模式串中a1字符命中,那么我们通过二进制位记录将得到1_0_0_1_0_0

以目标文件名“xyz7890”为例,我们初始化一个对应模式串长度的二进制位,0_0_0_0_0_0,逐字符扫描时,发现模式串均未命中,那么我们通过二进制位记录将得到0_0_0_0_0_0

以目标文件名“231cbaa”为例,我们初始化一个对应模式串长度的二进制位,0_0_0_0_0_0,逐字符扫描时,发现模式串所有字符命中,那么我们通过二进制位记录将得到1_1_1_1_1_1

有限的二进制位可以方便地用int32int64存储,已经相当够用了。这种二进制编码的方式不仅节省了存储空间,还大大提高了搜索效率。布尔运算(如或运算)的速度远超字符串操作,因为它们直接在内存的位级别上进行操作,而字符串操作则需要逐字符比较和处理。

(三)关键词初筛

以目标文件名“apple 1”为例,此时的二进制位为1_0_0_1_0_0 = 36int值表示)

  • 我们搜索关键词“apel”,与模式串匹配得到关键词的二进制位1_0_0_1_0_0 = 36(十进制)
  • 我们搜索关键词“bpel”,与模式串匹配得到关键词的二进制位0_1_0_1_0_0 = 20(十进制)

对关键词与目标文件名做或运算:

int index_originTxt; // 假设已初始化
int index_keyTxt; // 假设已初始化
if (index_originTxt | index_keyTxt != index_keyTxt) return "索引初筛失败";

如果或运算后的值不等于原值,则表示关键词与目标文件名在模式串中存在不包含的字符。

对于多个关键词:

int index_originTxt;  //文件名
int index_keyTxt_1; // 搜索关键字1,假设已初始化
int index_keyTxt_2; // 搜索关键字2,假设已初始化
int index_keyTxt_3; // 搜索关键字3,假设已初始化
int index_keyTxt_final = index_keyTxt_2 | index_keyTxt_3; if (index_originTxt | index_keyTxt_final != index_keyTxt)
{
return "索引初筛失败";
}
else
{
// ...正常文本搜索
}

intlong的或运算非常轻量,速度远超string.Contains,且关键词越多,文件名越长,过滤效果越好。这种初筛机制可以快速排除大量不匹配的文件名,从而显著减少后续精确匹配的计算量,提高整体搜索效率。

底层技术细节:

  • 初筛机制: 使用位运算进行初筛,可以快速排除大量不匹配的文件名,减少后续精确匹配的计算量。
  • 性能优势: 位运算的速度极快,适合大规模数据的快速筛选。

三、关键词高亮(v1.1.7新增)

在 Avalonia 中,文件名的显示通过 TextBlock 控件实现,其 Inlines 属性绑定至一个自定义的高亮转换器.

<TextBlock Inlines="{Binding FileName, Converter={StaticResource HighlightConverter}}" />

该转换器类需实现 IValueConverter 接口,由 Avalonia 依赖注入容器自动实例化。其核心方法是 Convert,负责将原始文本按匹配的关键词切分,并构造高亮显示的 InlineCollection。具体实现如下:

  • 初始化一个 InlineCollection 对象用于存放文本段;
  • 遍历预处理后的关键词匹配结果(通常需先按起始位置排序,并合并相邻或重叠区间);
  • 对非匹配区域,添加普通 Run 对象显示文本;
  • 对匹配区域,创建 Run 对象并设置高亮画刷(如 Brushes.Yellow)以改变前景色;
  • 需注意多个关键词可能引起的区间重叠与重复问题,需通过算法(如区间合并)确保每个字符只处理一次。
// 创建行内元素集合,用于存储文本元素(Run)
var inlines = new InlineCollection(); // 遍历所有排序整合后的搜索结果
foreach (var result in results)
{
// 从原始文本中提取当前片段的子字符串
// result.Start: 片段起始位置
// result.Length: 片段长度
var textSegment = nameOrigin.Substring(result.Start, result.Length); // 创建文本元素(Run),用于显示文本片段
var run = new Run(textSegment); // 如果当前片段是匹配项,则应用高亮样式
// result.IsMatch: 标识该片段是否为搜索匹配项
if (result.IsMatch)
{
// 设置前景色为高亮画刷,突出显示匹配文本
run.Foreground = highlightBrush; // 可选:添加其他高亮样式,如加粗、背景色等
// run.FontWeight = FontWeight.Bold;
// run.Background = Brushes.Yellow;
} // 将文本运行添加到行内集合中
inlines.Add(run);
} // 返回构建完成的行内元素集合
// 该集合可在Avalonia TextBlock等控件中直接使用,显示带有高亮效果的文本
return inlines;

需要注意的是,多个关键词高亮后,需要对substring的区域进行重排以及去重/结合。这种设计不仅保证了高亮显示的准确性,还避免了重复处理同一段文本,从而提高了渲染效率。通过这种方式,用户可以清晰地看到搜索关键词在文本中的位置,提升了用户体验。

所有的Converter都是在虚拟模式下按需执行的,可以较好满足我们的性能需求.

四、最后

对于TDS搜索软件的其他信息信息可见此公众号的文章 https://mp.weixin.qq.com/s/inD-brKhii57UJnCYLgxKQ

目前关于TDS搜索的版本已经更新到了1.1.7, 优化了很多细节,增加了高亮,磁盘缓存索引,更多选项等一些功能. 欢迎大家拿走,分享,使用.

如果你对这款工具有任何建议或想法,欢迎随时交流!项目已在 GitHub 完全开源.

如果你觉得有用,欢迎点个 Star ️支持一下! https://github.com/LdotJdot/TDS

从零开始:C# 拼音首字母搜索、字符串编码、关键词高亮的原理即实现考虑的更多相关文章

  1. 【Solr】 solr对拼音搜索和拼音首字母搜索的支持

    问:对于拼音和拼音首字母的支持,当你在搜商品的时候,如果想输入拼音和拼音首字母就给出商品的信息,怎么办呢? 实现方式有2种,但是他们其实是对应的.  用lucene实现 1.建索引, 多建一个索引字段 ...

  2. iOS拼音搜索,拼音首字母搜索

    扩展了一下 搜索框,能够实现拼音和首字母模糊搜索 基本搜索 上一篇文章 #import "NSString+utility.h" @interface WJWPinyinSearc ...

  3. MySQL通过函数获取字符串汉字拼音首字母大写字符串

    DELIMITER $$ DROP FUNCTION IF EXISTS `Fun_GetPY`$$ CREATE FUNCTION `HIS`.`Fun_GetPY` (in_string VARC ...

  4. MVC+Jquery+autocomplete(汉字||拼音首字母搜索)

    最近项目中用到了autocomplete了,总结一下经验. 我们先来看一下效果:

  5. java获取多个汉字的拼音首字母

    本文属于http://java.chinaitlab.com/base/803353.html原创!!! public class PinYin2Abbreviation { // 简体中文的编码范围 ...

  6. 算法笔记_232:提取拼音首字母(Java)

    目录 1 问题描述 2 解决方案   1 问题描述 在很多软件中,输入拼音的首写字母就可以快速定位到某个词条.比如,在铁路售票软件中,输入: “bj”就可以定位到“北京”.怎样在自己的软件中实现这个功 ...

  7. Java实现提取拼音首字母

    在很多软件中,输入拼音的首写字母就可以快速定位到某个词条.比如,在铁路售票软件中,输入: "bj"就可以定位到"北京".怎样在自己的软件中实现这个功能呢?问题的 ...

  8. asp.net 获取汉字字符串的拼音首字母,含多音字

    需求:在很多时候数据查询的时候,我们希望输入某个人姓名的拼音首字母进行查询,例如“潘长江”,输入“pcj”,就能搜索潘长江相关信息. 实现: #region 获取汉字转换拼音 首字母 public s ...

  9. 取字符串拼音首字母(js)

    //取字符串拼音首字母 function makePy(str) { if (typeof(str) != "string") throw new Error(-1, " ...

  10. Elasticsearch高级搜索排序( 中文+拼音+首字母+简繁转换+特殊符号过滤)

    一.先摆需求: 1.中文搜索.英文搜索.中英混搜   如:"南京东路","cafe 南京东路店" 2.全拼搜索.首字母搜索.中文+全拼.中文+首字母混搜   如 ...

随机推荐

  1. mysql索引优化解决方案

    mysql索引优化解决方案(在b站动力节点学习的) 可能因为这个视频是比较新的视频,评论区都没有什么笔记和文档.于是我就跟着视频边学边记录笔记.希望有些建表的代码,有需要的可以直接复制,减少了大家的无 ...

  2. java ‘方法’简解

    status 1.静态变量要定义在方法之外 2.静态变量与静态方法都是在类从磁盘加载至内存后被创建的,与类同时存在,同时消亡. 3.静态变量又称类的成员变量,在类中是全局变量,可以被类的所有方法调用 ...

  3. CLTX 笔试题目预览

    error: multiple storage classes in declaration of `i' 代理服务器常用以下端口: (1). HTTP协议代理服务器常用端口号:80/8080/312 ...

  4. AD 提权-NTLM 中继攻击(诱导认证)

    我醉欲眠卿且去,明朝有意抱琴来. 导航 0 前言 1 实验环境 2 SMB 转 SMB 3 SMB 转 LDAP 4 SMB 转 HTTP 5 HTTP 转 SMB 6 HTTP 转 LDAP 7 杂 ...

  5. JsonConvert反序列化枚举转换

    适用场景:反序列化对象里面存在枚举类型 说明 在使用JsonConvert.DeserializeObject转换对象的时候,想要更直观的看到值对应的含义,一般会设定一个枚举值,但是在转换的时候,由于 ...

  6. P6429 [COCI2008-2009#1] JEZ 题解

    题目传送门:Click. 更好地观感:Click(进入速度玄学) 某蒟蒻看见这道题,想了足足一个晚上,过后茅塞顿开,故作此篇. 感谢神犇的题解,思路基本相同,补充了一些自己的想法或这片题解可能没有注意 ...

  7. 达人篇:4)IATF16949 --寻找其中设计的内容

    本章目的:了解IATF16949流程五大工具及对应关系. 五大工具的关系图

  8. C# Lua 获取指定字符中间的字符串 正则表达式获取括号里面的字符串

    实例代码:获取 [] 里面的内容 Lua版本 print('-----------------') for s in string.gmatch('pp[1g1]ppp[1jj2]pp[1413]pp ...

  9. [J组模拟赛 #002 T4]分组选数

    分组选数 题目大意: 给 \(n\) 个数,第 \(i\) 个数是 \(a_i\),属于第 \(b_i\) 个集合中.对于每个集合,若从中选出若干个数,则价值为这些数的异或和,总共的价值就是所有集合的 ...

  10. 如何设计一个RPC框架 需要考虑哪些问题