_analyzer_ik_max_word_ik_smart,如果没有配置标准分词器,会导致写入和查询分词不一致,进而影响召回。
配置示例:
PUT /article_index { "mappings": { "properties": { "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" } } } }3.2 必知必会的映射参数
| 参数名 | 含义 | 实战建议 |
|---|---|---|
analyzer | 写入时使用的分词器 | 中文场景用ik_max_word或ik_smart |
search_analyzer | 查询时使用的分词器 | 一般和analyzer保持一致,或选择粒度更粗的版本 |
fields | 多字段 | 给 text 挂 keyword 子字段,解决排序聚合 |
index | 是否索引 | 不需要搜索的字段设为false,省空间 |
norms | 是否存储归一化因子 | 纯过滤字段设为false,省内存 |
boost | 权重 | 建议在查询时指定,不在映射里写死 |
ignore_above | 超过长度的值不索引 | 用在 keyword 子字段,避免超长文本导致索引膨胀 |
fields是我最常用的一个参数。业务里几乎每个 text 字段都需要一个 keyword 子字段来兜底,配置方式如下:
PUT /product_index { "mappings": { "properties": { "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } }配置完成后,name用于全文检索,name.keyword用于精确匹配、排序、聚合。这是解决 text 类型功能限制的标准方案,没有之一。
3.3 映射之所以不能修改,和倒排索引的关系
很多人不理解为什么 ES 的映射不能被修改,只是简单记住"不能改"。从原理上看,text 字段的分词结果已经固化在倒排索引里,如果中途修改分词器,旧的倒排索引和新词条无法兼容,等于数据白写了。这就是为什么 ES 只允许新增字段,不允许修改已有字段的类型和分词配置。
因此,映射设计必须一次到位。我在实践中的做法是:索引名带版本号,比如article_v1、article_v2,需要变更映射时新建索引并通过别名切换,尽量降低迁移成本。如果直接在旧索引上改映射,只能全量重建数据,非常痛苦。
4. 写入和查询时,text 字段到底做了什么
4.1 写入链路
向索引写入一条包含 text 字段的文档时,ES 依次执行:
- 根据 mapping 中该字段的
analyzer配置加载分词器链; - 对原始文本执行字符过滤、分词、词条过滤;
- 将最终词条写入倒排索引;
- 如果配置了
fields,同步为子字段建立索引。
这里我要特别强调一个新手经常搞混的点:分词不会改变_source里的原始内容。_source保存的是你写入时的原始 JSON 文档,查询时返回的也是原样。分词结果只存在于倒排索引中,用于匹配和打分,不会覆盖原始数据。
所以你在 Kibana 里看文档数据,看到的永远是原始字符串,不要因此误以为分词器没生效。验证分词器是否生效的唯一方法是调用_analyzeAPI,后面会专门讲。
4.2 查询链路
对 text 字段执行match查询时,ES 的处理路径是:
- 查询词先经过
search_analyzer分词,变成词条集合; - 每个词条在倒排索引中查找命中的文档 id 集合;
- 对命中文档计算 BM25 相关性分数;
- 按分数降序返回。
对 keyword 字段执行term查询时,没有分词这一步,拿着原始值直接去倒排索引匹配。这就是"text 用 match,keyword 用 term"这句话的技术根源。
4.3 一个简单的写入和查询联动实验
为了说清楚,我举一个极简的示例。索引里写一条文档:
POST /demo_index/_doc/1 { "content": "Elasticsearch 全文检索实践" }content字段是 text 类型,使用默认standard分词器。写入后倒排索引里大概有这些词条:elasticsearch、全、文、检、索、实、践。注意,标准分词器对中文是逐字切分的。
此时用match搜索"全文检索",查询词会被切成全、文、检、索四个单字,和索引里的词条匹配,文档被召回,但相关性分数并不高,因为匹配粒度太细。
这个实验很好地解释了为什么中文场景默认分词器表现差。把问题可视化,比看任何文档都直观。
5. 使用 text 字段最常踩的坑和排查思路
5.1 中文搜索召回率低,搜索结果不相关
现象:搜索"搜索引擎优化",召回了一堆只包含"搜索"或"引擎"的文档,语义完全不相关。
原因:standard分词器没有中文分词能力,文本被切成单个汉字,查询和索引的匹配粒度都太细,相关性大幅失真。
排查方法:用_analyze接口直接看分词结果:
POST /_analyze { "analyzer": "standard", "text": "搜索引擎优化" }输出是搜、索、引、擎、优、化这样的单字序列,基本可以断定问题出在分词器上。换成ik_max_word后,分词结果会变成搜索引擎优化、搜索引擎、搜索、引擎、优化等有意义的词条,搜索结果立刻不一样。
5.2 text 字段排序和聚合直接报错
现象:对 text 字段执行sort或aggs,ES 返回:
Fielddata is disabled on text fields by default原因:text 字段被分词后没有完整的原始值,无法用于排序和聚合。ES 默认关闭了 text 字段的 fielddata,就是为了防止爆内存。
排查方法:不要试图开启fielddata。我在生产环境见过对 text 开启 fielddata 的团队,堆内存直接打满,集群频繁 GC,最后只能回滚。正确做法是给 text 字段配置 keyword 子字段,排序和聚合一律用字段名.keyword。
5.3 term 查询和 match 查询混用导致结果不符合预期
现象:有人用term查询 text 字段,有时候能查到,有时候查不到,觉得玄学。
原因:term不做分词处理。如果倒排索引里存的是小写的elasticsearch,而查询传入Elasticsearch,大小写不一致直接匹配失败。这和字段内部存储的词条天然不匹配有关。
排查方法:记住一条原则——text 字段用match,keyword 字段用term。不要跨类型混用,除非你完全清楚自己在做什么。
5.4 大小写和中英文混合导致召回异常
现象:索引里存的文本是Elasticsearch 实战,搜索elasticsearch召回正常,搜索Elasticsearch却召回不了。
原因:standard分词器默认包含lowercase过滤器,写入时英文统一变小写。如果索引用的分词器链不包含 lowercase,或者查询时传入了大小写混合的词,就会出现这种不一致。
排查方法:检查字段的分词器链是否包含lowercase过滤器,_analyze接口一测就知道。
5.5ignore_above参数引发的"丢数据"错觉
现象:keyword 子字段设置了ignore_above: 256,超过 256 字符的内容无法在子字段上做精确匹配,但主 text 字段还能搜到。
原因:ignore_above的作用是超过指定长度的字符串不索引。对于超长文本,keyword 子字段里没有值,term 查询自然查不到。
排查方法:这不算 bug,是配置的预期行为。如果需要超长文本也能精确匹配,调大ignore_above,但要注意 keyword 字段长度过大时内存占用会上升。
5.6 用了 IK 分词器但新词不生效
现象:添加了新词到 IK 自定义词库,重启 ES 后_analyze测试切分效果,依然不识别。
排查方法:IK 词库有热更新机制,但需要配合 HTTP 接口或远程词库配置,并确保 ES 节点的响应时间足够。如果只是把词丢进本地文件然后重启,不一定生效。正确步骤是先配置远程词库地址,再触发定时加载,最后用_analyze验证。这个坑的特点是配置不报错,但词就是切不对,很容易让人怀疑分词器本身有问题。
6. text 字段相关的几个核心操作示例
6.1 验证分词结果的_analyze接口
_analyze是排查分词问题的第一工具,建议每次配置完分词器都测一遍。一个常用的快速验证命令:
POST /article_index/_analyze { "field": "content", "text": "Elasticsearch 全文检索实践" }注意这里指定了field,表示使用content字段在 mapping 里配置的分词器来分析,而不是单独指定analyzer参数。输出结果会展示每一个词条以及它在原始文本中的位置偏移量。
这个接口我在排查问题时的使用频率非常高。任何搜索质量异常,第一步都是确认分词是否符合预期,而不是先去调查询 DSL。
6.2 查询时的常用 DSL 写法
text 字段的常用查询主要有四种:match、match_phrase、multi_match和query_string。
match是默认的全文查询,分词后做 OR 匹配:
POST /article_index/_search { "query": { "match": { "content": "全文检索" } } }match_phrase要求词条按顺序连续出现,适合短语匹配:
POST /article_index/_search { "query": { "match_phrase": { "content": "全文检索实践" } } }multi_match可以同时匹配多个 text 字段,并设置不同权重。比如让标题匹配的权重高于正文:
POST /article_index/_search { "query": { "multi_match": { "query": "全文检索", "fields": ["title^2", "content"] } } }^2表示 title 字段的得分权重翻倍。这是我在实际搜索功能里最常用的一种组合,因为业务需求往往是"标题优先,正文其次"。
6.3 高亮显示命中片段
全文检索场景几乎离不开高亮。ES 原生支持高亮,配置非常简单:
POST /article_index/_search { "query": { "match": { "content": "全文检索" } }, "highlight": { "fields": { "content": {} } } }返回结果里会多出highlight字段,命中片段会用<em>标签包裹。前端直接渲染即可,不需要额外处理。
6.4 一个完整的业务场景配置
假设做一个内容管理系统,文章包含标题、正文、标签、状态、发布时间,需求是标题和正文支持全文检索,标签和状态支持精确过滤,标题要支持精确排序。映射可以这样设计:
PUT /cms_article { "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": { "keyword": { "type": "keyword", "ignore_above": 128 } } }, "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "tag": { "type": "keyword" }, "status": { "type": "keyword" }, "publish_date": { "type": "date" } } } }对应的查询组合示例:
POST /cms_article/_search { "query": { "bool": { "must": [ { "match": { "title": "全文检索" } } ], "filter": [ { "term": { "status": "published" } }, { "term": { "tag": "es" } } ] } }, "sort": [ { "title.keyword": { "order": "asc" } } ] }这个查询里,全文检索走title主字段,精确过滤走status和tag的 keyword 字段,排序走title.keyword。这是生产环境中出现频率最高的组合形式之一。
7. text 与相关类型的对比选型
7.1 text、keyword、match_only_text
| 维度 | text | keyword | match_only_text |
|---|---|---|---|
| 分词 | 是 | 否 | 是,但不记录词位置 |
| 倒排索引 | 有 | 有 | 有,不记录位置信息 |
| 精确匹配 | 不支持 | 支持 | 不支持 |
| 排序/聚合 | 不支持 | 支持 | 不支持 |
| 短语查询 | 支持 | 不支持 | 不支持 |
| 典型场景 | 标题、正文搜索 | 标签、ID、状态 | 日志匹配,不需要短语搜索 |
match_only_text是 ES 里一个存在感不高的类型,它也是分词索引的,但不保存词的 position 信息,因此无法支持match_phrase短语查询。优点是索引体积更小、内存占用更低。对于日志场景,如果你只需要判断"某条日志里有没有出现某个词",不需要短语查询,用match_only_text比 text 更合适。
7.2 一套简单的选型判断流程
我的判断流程很简单:
- 需要全文检索、相关度排序、模糊匹配 → 选 text;
- 需要精确匹配、聚合、排序 → 选 keyword;
- 两者都要 → text 主字段 + keyword 子字段;
- 只存储不检索 → keyword 且
index: false; - 日志类只做命中匹配 → 考虑
match_only_text。
这个流程覆盖了我见过的绝大多数业务需求。真正用到wildcard或其他特殊类型的场景,往往是有更复杂的匹配诉求,不在本篇范围。
8. 生产环境中的几点实操经验
8.1 搜索质量回归用例
文本搜索最大的问题不是写不出来,而是改着改着就把搜索质量改差了。我习惯在测试环境维护一组"搜索质量回归用例",把核心搜索词和期望的结果 id 写死,每次调整映射、分词器或查询 DSL 后跑一遍,确保核心搜索词的结果没有回退。这些用例在分词器升级或索引重建时尤其有用。
8.2 写入性能和分词粒度的取舍
text 字段分词过程消耗 CPU,分词越细,索引越大,写入越慢。我在一次压测里对比过相同数量文档的写入耗时,使用standard分词器的 text 字段比 keyword 字段慢约 15%,使用ik_max_word则慢约 30%。如果业务里某个字段只是存储一段文本,不需要全文检索,务必用 keyword 加index: false,别让无谓的分词消耗集群资源。
8.3 聚合和排序永远走 keyword 子字段
我在团队里定了一条规矩:text 字段不允许直接开 fielddata,任何排序和聚合需求,统一走 keyword 子字段。这条规矩避免了大量内存问题。如果你接手了一个老索引,发现 text 字段没有 keyword 子字段,但又有排序聚合需求,想办法重建索引,而不是开 fielddata 走捷径。
8.4 排查顺序可以固定下来
我在处理搜索类问题时,排查顺序固定为三步:
- 看 mapping,确认字段类型和分词器配置是否符合预期;
- 用
_analyze看实际分词结果,确认写入和查询分词是否一致; - 看查询 DSL,确认使用的查询类型(match、term、match_phrase)是否匹配字段类型。
这个顺序基本能在几分钟内定位绝大多数问题。多数所谓的"搜索不准",最终都会落到两个原因:分词器没配对,或者查询类型用错了字段。
8.5 关于index: false和存储的关系
最后提一个容易混淆的点:index: false只是不建立倒排索引,并不代表字段不存储。_source里依然会有完整原始值。如果你既不想检索这个字段,又不想存储它,那要在 mapping 层面配合_source的 includes/excludes 来控制,这是另一套机制。不要把index: false当作"删掉字段"的手段。
text 类型是 ES 全文检索能力的核心,但它的使用绝不是"把字段配成 text 就够了"。分词器的选择、多字段的设计、查询类型的匹配、索引体积的控制,每一样都需要结合业务场景仔细权衡。希望这篇文章能帮你在实际项目中把 text 字段用得明明白白。