news 2026/9/9 23:57:43

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 MEP(Milvus 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 配置里启用"pinyin"filter

该过滤器以新的 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输出中保留原始中文 token
keep_full_pinyinbooltrue把每个汉字单独输出为对应拼音 token(例:"中文" →zhongwen
keep_joined_full_pinyinboolfalse把整词所有汉字的拼音拼成一个连续 token(例:"中文" →zhongwen
keep_separate_first_letterboolfalse把整词每个字拼音首字母拼成一个 token(例:"中文" →zw

2.2 字符串简写形式

当不需要任何定制时,可以直接把"pinyin"作为字符串写进 filter 数组,此时使用全部默认选项(即keep_original=truekeep_full_pinyin=true、其余为false)。MEP 文档明确说明:“When used with no parameters (i.e.,"pinyin"as a plain string filter), the default options apply.”

从源码看,这一简写确实落到了SystemFilterFrom<&str>分支:

"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: xxxno 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 文档把实现拆成三层,源码中一一对应:

  1. PinyinFilter—— 实现tantivy::tokenizer::TokenFiltertrait,内部只保存一份PinyinOptions配置;其transform()负责把上游 tokenizer 包装成新的 tokenizer。
  2. PinyinFilterWrapper<T>—— 泛型包装器,Tokenizer实现里创建出实际的 token 流对象并持有一份克隆的PinyinOptions(见 pinyin_filter.rs)。
  3. PinyinFilterStream<T>—— 真正执行转换的 token 流:通过缓存队列 + 游标方式,把上游进来的 1 个 token 展开成多个输出 token(cache: Vec<Token>+index: 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_json(analyzer.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):

  1. keep_original=true,先把原始 token 原样压入缓存队列;
  2. 遍历 token 文本的每一个字符,通过pinyincrate 的ToPinyintrait 转换(to_pinyin().flatten())——注意flatten()意味着无法转写的字符会被静默跳过,天然实现“只处理汉字、忽略非中文字符”;
  3. 依配置产出派生 token:
    • keep_full_pinyin=true:每字拼音作为独立 token(char.plain());
    • keep_joined_full_pinyin=true:逐字拼接进join_pinyin,非空才整体压入一个 token;
    • keep_separate_first_letter=true:逐字取char.first_letter()拼进first_letter,非空才压入;
  4. 空转写结果(纯 ASCII/数字等 token)不会生成任何空 token。

4.2 一个值得注意的源码级细节:offset 与 position 的精确语义

MEP 文档概述称“All generated tokens share the sameoffset_from,offset_to, andpositionas the original token”。对照真实源码需要做一处更精确的说明:offset 确实完全继承原 token,但position 并非一律相同——

  • 逐字全拼 token(keep_full_pinyin)的 position 会按字序号递增:start_position = token.position + index(仅当index <= position_length),并且position_length强制设为1,其意图是让逐字拼音可作为相互独立的词位参与短语/临近匹配;
  • 而整词拼接 token(keep_joined_full_pinyinkeep_separate_first_letter)则原样沿用原 token 的positionposition_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 输出如下:

配置输出 tokens
keep_original=true, keep_full_pinyin=true中文zhongwen测试ceshi
keep_original=true, keep_joined_full_pinyin=true中文zhongwen测试ceshi
keep_original=true, keep_separate_first_letter=true中文zw测试cs
全部选项开启中文zhongwenzhongwenzw测试ceshiceshics

可以把上表理解为“索引侧倒排里每种形态各占一个词项”,查询文本在查询侧也会走同样的展开逻辑——这正是查询“中文”“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.go):

func 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_pinyin=false+keep_joined_full_pinyin=true,即只为每词保留一个整词拼音(如zhongwen),刻意不产生逐字拼音与首字母形式——这正好用来验证“没开的形态不会被命中”。

6.2 用 RunAnalyzer 直接观察分词结果(免建索引排障)

写入前就能用RunAnalyzer把 analyzer 实际跑一遍、核对展开后的 token(pinyin_filter_test.go):

results, 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_original=true时“中文测试”应输出["中文", "zhongwen", "测试", "ceshi"],单独一个“中文”输出["中文", "zhongwen"];而当keep_original=false时输出只剩["zhongwen", "ceshi"](见 pinyin_filter_test.go)。这直观印证了第 5 节的表格,也是日常排查“为什么某拼音查不到”的首选工具。

6.3 写入、建索引后按拼音检索

测试覆盖了 sealed(已封口/已索引段)、unsealed(未索引封口段)与 growing(增长段)三类数据路径:先写入 3000 行、flush 成已索引 sealed 段,再写 500 行封口成未建索引 sealed 段,加载集合后再写入 500 行增长段。检索统一用全文匹配函数text_match作为 Search 的过滤条件(pinyin_filter_test.go):

filter := fmt.Sprintf("text_match(text, %q)", queryText) // 也可指定 minimum_should_match: // text_match(text, "中文", minimum_should_match=2) 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(子集匹配)做断言:

  1. 整词全拼keep_joined_full_pinyin=true→ 期望包含zhongwenceshi
  2. 逐字全拼keep_full_pinyin=true→ 期望包含zhongwenceshi
  3. 首字母keep_separate_first_letter=true→ 期望包含zwcs

单测与上述 E2E 用例在输入“中文测试”上完全一致,形成了“Rust 过滤逻辑 ↔ SDK 端到端行为”的双层证据闭环。

7. 兼容性、迁移与选型建议

  • 完全向后兼容、纯增量特性:不修改任何既有 analyzer 语义,MEP 声明对现有配置无影响;已有集合无需迁移。用户只需在 analyzer 配置里主动添加"pinyin"filter 即可“opt-in”启用。
  • 二进制体积影响可控:新增依赖仅pinyin = "0.10"一个 crate,MEP 评估为“slightly increases compiled binary size”,对部署影响很小。
  • 推荐组合(供选型参考):若目标是中文姓名/地名拼音检索,通常建议keep_joined_full_pinyin=true(支持整词拼音,如zhangsanbeijing),并搭配keep_original=true保住原文匹配;若还需要“首字母缩写”检索(类似输入法声母联想zs),再加keep_separate_first_letter=true;若需要容纳“拼音逐字匹配长词中某个字”,则开keep_full_pinyin=true。三个开关也可全开,代价只是倒排词项数变多。
  • 注意事项:拼音转换只作用于汉字字符;数字、拉丁字符等无法转写的部分会被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.rs
  • analyzer 解析入口:internal/core/thirdparty/tantivy/tantivy-binding/src/analyzer/analyzer.rs
  • 依赖声明(pinyin = "0.10"):internal/core/thirdparty/tantivy/tantivy-binding/Cargo.toml
  • Go 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:55:21

C11 _Generic 宏:编译期类型选择实战指南

在C语言项目里&#xff0c;凡是遇到“同一种操作&#xff0c;不同类型不同实现”的需求&#xff0c;最尴尬的事就是——你明明只有一个宏&#xff0c;却要写出好几个分支&#xff0c;或者干脆忍受编译器那一声不痛不痒的警告。比如早期我用printf打印变量时&#xff0c;%d配dou…

作者头像 李华
网站建设 2026/9/9 23:54:32

深入理解Android IdleHandler:原理、实战与避坑指南

开头说到 IDLE Handler&#xff0c;做 Android 开发的朋友应该都不陌生&#xff0c;尤其是搞过启动优化、卡顿治理的同学&#xff0c;肯定跟它打过交道。这货是 MessageQueue 里的一个内部接口&#xff0c;从名字就能看出来&#xff0c;它是用来处理“空闲时间”的。说白了&…

作者头像 李华
网站建设 2026/9/9 23:53:00

微电网多时间尺度调度:PSO+MPC三级协同优化实战

1. 先搞明白&#xff1a;为什么单一时间尺度调度在含新能源场景下扛不住 接手这个课题之前&#xff0c;我其实先后踩过两个方向的弯路。最初我也和大家一样&#xff0c;拿到"多时间尺度联合调度"的题目&#xff0c;第一反应是找几篇综述&#xff0c;把日前、日内、超…

作者头像 李华
网站建设 2026/9/9 23:52:00

高职大数据工程技术专业:学什么、怎么学、如何就业

1. 这个专业&#xff0c;到底在学什么 先说结论&#xff1a;高职大数据工程技术&#xff0c;不是让你去搞人工智能算法的&#xff0c;也不是培养科学家的&#xff0c;它培养的是能把数据“管起来、跑得动、看得见”的工程型人才。 很多同学填志愿的时候&#xff0c;看到“大数…

作者头像 李华