ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Milvus 文本分析器 Pinyin Filter(拼音过滤器):从配置到端到端搜索实践

Milvus 文本分析器 Pinyin Filter(拼音过滤器):从配置到端到端搜索实践 Milvus 文本分析器 Pinyin Filter拼音过滤器从配置到端到端搜索实践【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus本文对应仓库设计文档docs/design-docs/design_docs/20260209-pinyin_filter.md即 Milvus MEPMilvus Enhancement Proposal中关于“Pinyin Filter for Text Analyzer”的实现提案。导读本文深入介绍 Milvus 全文检索文本分析器中的内置Pinyin Filter拼音过滤器它能把中文分词后的汉字 token 自动转写成拼音拉丁字母让用户直接用拼音输入即可命中中文内容支撑人名/地名检索、输入法拼音联想search-as-you-type、以及无中文输入法环境下的跨输入法搜索等场景。读完本文你将掌握该过滤器在 Milvus 配置 JSON 中的全部参数与默认值、它在 tantivy 分词管线底层的 token 展开实现原理并能够基于官方测试用例在 Go SDK 中端到端地创建启用了拼音过滤的集合并用text_match完成中/拼音混合搜索。1. 背景与动机为什么需要在全文检索管线里做拼音转换Milvus 对中文全文检索的支持此前依赖于 Jieba 等分词器完成“词切分”但分词产物始终是汉字本身没有任何内置手段用拼音输入去命中中文内容。这对大量中文场景是硬需求姓名/地名检索用户习惯敲拼音例如输入zhangsan期望命中“张三”输入beijing期望命中“北京”自动补全与边打边搜绝大多数设备的输入法是把拼音按键流转成汉字若索引与查询两侧都能按拼音匹配搜索体验会更快更自然跨输入法检索部分用户环境没有中文输入法只能使用拉丁字符检索中文数据。没有拼音过滤器时用户只能自维护一个拼音映射字段或在应用层做转换既增加写入侧复杂度与存储开销又难以保证两端转换规则一致。将其实现为“分词管线内的一个 filter”则可以在索引构建写入与查询改写两侧天然复用同一套逻辑属于更优雅的方案。这也在该 MEP 的“Rejected Alternatives被否决的备选方案”中得到了印证应用层维护拼音字段复杂且有存储开销而独立拼音分词器不如 filter 可组合——filter 可以叠加在 Jieba、standard 等任意分词器之后再与停用词、小写化等其它 filter 串联。2. 公共接口在 Analyzer 配置里启用pinyinfilter该过滤器以新的 filter 类型pinyin暴露在 analyzer 配置 JSON 中可挂载到任意 analyzer 的 filter 管线。下面配置即官方 MEP 文档与 Rust 单测pinyin_filter.rs使用的形态{ tokenizer: jieba, filter: [ { type: pinyin, keep_original: true, keep_full_pinyin: true, keep_joined_full_pinyin: false, keep_separate_first_letter: false } ] }2.1 四个布尔参数的含义与默认值参数类型默认值说明keep_originalbooltrue输出中保留原始中文 tokenkeep_full_pinyinbooltrue把每个汉字单独输出为对应拼音 token例中文 →zhong、wenkeep_joined_full_pinyinboolfalse把整词所有汉字的拼音拼成一个连续 token例中文 →zhongwenkeep_separate_first_letterboolfalse把整词每个字拼音首字母拼成一个 token例中文 →zw2.2 字符串简写形式当不需要任何定制时可以直接把pinyin作为字符串写进 filter 数组此时使用全部默认选项即keep_originaltrue、keep_full_pinyintrue、其余为false。MEP 文档明确说明“When used with no parameters (i.e.,pinyinas a plain string filter), the default options apply.”从源码看这一简写确实落到了SystemFilter的Fromstr分支pinyin Self::Pinyin(PinyinFilter::default()),见 filter.rs而 JSON 对象形式则由create_filter中pinyin PinyinFilter::from_json(params)分支负责见 filter.rs。使用提示前提与限制以上配置适用于启用全文检索能力VARCHAR 字段开启 analyzer match的集合字段不同语言 SDK 的“启用 analyzer”开关名称略有差异Go 侧为WithEnableAnalyzer(true).WithEnableMatch(true)具体见下文第 6 节。若 analyzer JSON 中 filter 元素既非字符串也非含type的 JSON 对象或type不是字符串、不属于已注册类型构建 analyzer 都会失败并返回明确错误如unsupport filter type: xxx、no type field in filter params这部分校验逻辑同样位于 filter.rs。3. 实现位置与整体架构拼音过滤器的实现位于 Milvus 为全文检索准备的 tantivy-binding Rust crate 内与 RegexFilter、SynonymFilter 等既有过滤器处于同一目录、同一种插件模式之下pinyin_filter.rs —— 核心过滤器实现filter.rs —— 在系统过滤器分发系统中完成注册mod.rs —— 模块声明与导出Cargo.toml —— 引入第三方依赖pinyin 0.10中文转拼音库。3.1 三个组成类型的职责MEP 文档把实现拆成三层源码中一一对应PinyinFilter—— 实现tantivy::tokenizer::TokenFiltertrait内部只保存一份PinyinOptions配置其transform()负责把上游 tokenizer 包装成新的 tokenizer。PinyinFilterWrapperT—— 泛型包装器Tokenizer实现里创建出实际的 token 流对象并持有一份克隆的PinyinOptions见 pinyin_filter.rs。PinyinFilterStreamT—— 真正执行转换的 token 流通过缓存队列 游标方式把上游进来的 1 个 token 展开成多个输出 tokencache: VecTokenindex: usize见 pinyin_filter.rs。配置解析入口PinyinFilter::from_json会逐项读取四个 key任何一项若传了非布尔值都会直接报错例如keep_original must be a boolean value未出现的 key 保持默认值见 pinyin_filter.rs。3.2 在全文检索整体链路中的位置从调用关系看该 crate 的create_analyzer/create_analyzer_by_jsonanalyzer.rs负责把 analyzer 配置 JSON 解析成 tantivy 的TextAnalyzer其中 filter 数组逐项生效字符串元素走SystemFilter::from对象元素走create_filter最后统一transform(builder)追加到管线见 analyzer.rs。这套 analyzer 被 Milvus 的 DataNode/QueryNode该 MEP 标记的 Component在写入侧建索引与查询侧解析用户文本时共用因此拼音过滤天然同时作用于“索引端分词”与“查询端分词”。4. 底层原理token 展开逻辑与细节校对4.1 处理流程PinyinFilterStream::advance()对上游如 Jieba传入的每个 token 执行如下处理见 pinyin_filter.rs若keep_originaltrue先把原始 token 原样压入缓存队列遍历 token 文本的每一个字符通过pinyincrate 的ToPinyintrait 转换to_pinyin().flatten()——注意flatten()意味着无法转写的字符会被静默跳过天然实现“只处理汉字、忽略非中文字符”依配置产出派生 tokenkeep_full_pinyintrue每字拼音作为独立 tokenchar.plain()keep_joined_full_pinyintrue逐字拼接进join_pinyin非空才整体压入一个 tokenkeep_separate_first_lettertrue逐字取char.first_letter()拼进first_letter非空才压入空转写结果纯 ASCII/数字等 token不会生成任何空 token。4.2 一个值得注意的源码级细节offset 与 position 的精确语义MEP 文档概述称“All generated tokens share the sameoffset_from,offset_to, andpositionas the original token”。对照真实源码需要做一处更精确的说明offset 确实完全继承原 token但position 并非一律相同——逐字全拼 tokenkeep_full_pinyin的 position 会按字序号递增start_position token.position index仅当index position_length并且position_length强制设为1其意图是让逐字拼音可作为相互独立的词位参与短语/临近匹配而整词拼接 tokenkeep_joined_full_pinyin、keep_separate_first_letter则原样沿用原 token 的position与position_length。if self.options.keep_full_pinyin { let mut start_position self.tail.token().position; if index self.tail.token().position_length { start_position start_position index; } self.cache.push(Token { text: char.plain().to_string(), offset_from: self.tail.token().offset_from, offset_to: self.tail.token().offset_to, position: start_position, position_length: 1, }) }见 pinyin_filter.rs。4.3 依赖选型转换依赖 pinyin Rust crate版本 0.10它提供不带声调的纯拼音plain()与首字母first_letter()两类输出正好覆盖本文档需要的全部三种拼音形态。其取舍无音调、按字转换也决定了该过滤器的定位是“辅助召回/联想”而非“语义理解”。5. 分词输出示例速查沿用 MEP 文档示例输入文本“中文测试”由 Jieba 分词为“中文”与“测试”两个 token 后不同配置组合的最终 token 输出如下配置输出 tokenskeep_originaltrue, keep_full_pinyintrue中文、zhong、wen、测试、ce、shikeep_originaltrue, keep_joined_full_pinyintrue中文、zhongwen、测试、ceshikeep_originaltrue, keep_separate_first_lettertrue中文、zw、测试、cs全部选项开启中文、zhong、wen、zhongwen、zw、测试、ce、shi、ceshi、cs可以把上表理解为“索引侧倒排里每种形态各占一个词项”查询文本在查询侧也会走同样的展开逻辑——这正是查询“中文”“zhongwen”“zw”都能命中同一批文档的根因。6. 端到端落地Go SDK 中的建集合、验词、检索配套仓库在 tests/go_client/testcases/pinyin_filter_test.go 提供了完整的 L0 级可合并进 CI 的轻量场景Go SDK 端到端用例可以直接当作使用范本。6.1 定义 analyzer 与集合用 Go 的字段属性开关 analyzer JSON 创建一个 VARCHAR 字段参与全文检索pinyin_filter_test.gofunc pinyinAnalyzerParams(keepOriginal bool) map[string]any { return map[string]any{ tokenizer: jieba, filter: []any{ map[string]any{ type: pinyin, keep_original: keepOriginal, keep_full_pinyin: false, keep_joined_full_pinyin: true, keep_separate_first_letter: false, }, }, } } // 建集合VARCHAR 字段开启 analyzer match并挂上含 pinyin filter 的 analyzer 参数 schema : entity.NewSchema().WithName(collectionName). WithField(entity.NewField().WithName(id).WithDataType(entity.FieldTypeInt64).WithIsPrimaryKey(true)). WithField(entity.NewField().WithName(text).WithDataType(entity.FieldTypeVarChar).WithMaxLength(1024). WithEnableAnalyzer(true).WithEnableMatch(true).WithAnalyzerParams(analyzerParams)). WithField(entity.NewField().WithName(vector).WithDataType(entity.FieldTypeFloatVector).WithDim(2))提示该用例中拼音开关组合是keep_full_pinyinfalsekeep_joined_full_pinyintrue即只为每词保留一个整词拼音如zhongwen刻意不产生逐字拼音与首字母形式——这正好用来验证“没开的形态不会被命中”。6.2 用 RunAnalyzer 直接观察分词结果免建索引排障写入前就能用RunAnalyzer把 analyzer 实际跑一遍、核对展开后的 tokenpinyin_filter_test.goresults, err : mc.RunAnalyzer(ctx, client.NewRunAnalyzerOption(中文测试). WithField(collectionName, text)) require.NoError(t, err) tokens : make([]string, len(results[0].Tokens)) for i, token : range results[0].Tokens { tokens[i] token.Text }用例断言keep_originaltrue时“中文测试”应输出[中文, zhongwen, 测试, ceshi]单独一个“中文”输出[中文, zhongwen]而当keep_originalfalse时输出只剩[zhongwen, ceshi]见 pinyin_filter_test.go。这直观印证了第 5 节的表格也是日常排查“为什么某拼音查不到”的首选工具。6.3 写入、建索引后按拼音检索测试覆盖了 sealed已封口/已索引段、unsealed未索引封口段与 growing增长段三类数据路径先写入 3000 行、flush 成已索引 sealed 段再写 500 行封口成未建索引 sealed 段加载集合后再写入 500 行增长段。检索统一用全文匹配函数text_match作为 Search 的过滤条件pinyin_filter_test.gofilter : fmt.Sprintf(text_match(text, %q), queryText) // 也可指定 minimum_should_match // text_match(text, 中文, minimum_should_match2) result, err : mc.Search(ctx, client.NewSearchOption(collectionName, limit, []entity.Vector{entity.FloatVector{0, 0}}). WithANNSField(vector). WithFilter(filter). WithOutputFields(id, text))关键断言矩阵pinyin_filter_test.go查询文本minimum_should_match期望结果语义验证zhongwen—命中 3 个目标行整词拼音可检索核心场景中文2命中 3 个目标行原文检索不受影响zhong—空结果未开启keep_full_pinyin逐字拼音被正确禁用zw—空结果未开启keep_separate_first_letter首字母被正确禁用可见拼音开关具备精确的启停语义开哪个开关、就只会命中哪种拼音形态不存在“漏禁”情况minimum_should_match参数则保证“中文”这类跨两个分词词项的查询能要求全部词项命中避免误召回。6.4 单元测试侧的三场景验证Rust 侧的单元测试与实现同文件的 pinyin_filter.rsmod tests同样覆盖三个场景且全部以 Jieba 为上游分词器、用is_subset子集匹配做断言整词全拼keep_joined_full_pinyintrue→ 期望包含zhongwen、ceshi逐字全拼keep_full_pinyintrue→ 期望包含zhong、wen、ce、shi首字母keep_separate_first_lettertrue→ 期望包含zw、cs。单测与上述 E2E 用例在输入“中文测试”上完全一致形成了“Rust 过滤逻辑 ↔ SDK 端到端行为”的双层证据闭环。7. 兼容性、迁移与选型建议完全向后兼容、纯增量特性不修改任何既有 analyzer 语义MEP 声明对现有配置无影响已有集合无需迁移。用户只需在 analyzer 配置里主动添加pinyinfilter 即可“opt-in”启用。二进制体积影响可控新增依赖仅pinyin 0.10一个 crateMEP 评估为“slightly increases compiled binary size”对部署影响很小。推荐组合供选型参考若目标是中文姓名/地名拼音检索通常建议keep_joined_full_pinyintrue支持整词拼音如zhangsan、beijing并搭配keep_originaltrue保住原文匹配若还需要“首字母缩写”检索类似输入法声母联想zs再加keep_separate_first_lettertrue若需要容纳“拼音逐字匹配长词中某个字”则开keep_full_pinyintrue。三个开关也可全开代价只是倒排词项数变多。注意事项拼音转换只作用于汉字字符数字、拉丁字符等无法转写的部分会被flatten()跳过但其原文仍会因keep_original或分词器自身行为保留不会被误删。另外过滤器产出的全是无音调纯拼音音调无关的模糊拼音本身就是其设计目标若需要拼音与汉字的语义消歧同音字仍需配合其它字段/模型手段。8. 延伸阅读本提案原始文档docs/design-docs/design_docs/20260209-pinyin_filter.md核心实现internal/core/thirdparty/tantivy/tantivy-binding/src/analyzer/filter/pinyin_filter.rs过滤器注册与分发internal/core/thirdparty/tantivy/tantivy-binding/src/analyzer/filter/filter.rsanalyzer 解析入口internal/core/thirdparty/tantivy/tantivy-binding/src/analyzer/analyzer.rs依赖声明pinyin 0.10internal/core/thirdparty/tantivy/tantivy-binding/Cargo.tomlGo SDK 端到端用例建集合/验词/检索全覆盖tests/go_client/testcases/pinyin_filter_test.go【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表