数据库基础体系 · 第 40/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
Elasticsearch Mapping 与分词:字段类型、Analyzer 和索引设计
在 Elasticsearch 中,“字段如何存储”和“文本如何被搜索”是两个相互关联、但不能混为一谈的问题:
- Mapping 描述字段的数据类型、索引方式、排序与聚合能力,以及对象之间的关系。
- Analyzer 描述文本进入倒排索引前如何被转换为词项,也描述查询文本如何被转换。
- 索引设计 则决定字段是否应该拆分、是否需要多种搜索方式、是否需要保留原文,以及索引生命周期内如何演进。
如果只把字段声明为 text 或 keyword,而不理解倒排索引和分析过程,通常会在以下场景中遇到问题:
- 搜索能命中,但排序或聚合失败;
- 中文、英文、数字和标点被错误切分;
- 修改 Mapping 后发现旧数据仍按旧规则搜索;
- 数组对象之间发生“跨对象匹配”;
- 动态映射把业务字段推断成不合适的类型;
- 为了支持一种查询方式,重复创建大量索引字段。
下面从数据流开始,逐步说明这些机制。
一、从 JSON 文档到索引:Mapping 和 Analyzer 位于哪里
向 Elasticsearch 写入一条文档时,可以把过程抽象为:
JSON 文档
│
├─ Mapping:确定字段类型和索引行为
│
├─ text 字段 ── Analyzer ── 词项、位置、偏移量 ── 倒排索引
│
├─ keyword、数值、日期等 ── 类型编码 ── 倒排索引 / Doc Values
│
└─ _source:保留原始 JSON,供返回和重建使用
这里有三个容易混淆的概念:
-
_source不是倒排索引
_source通常保存写入时的原始文档内容。搜索、排序和聚合主要依赖字段索引结构,而不是扫描_source。 -
字段是否出现在 Mapping 中,不等于字段是否可搜索
一个字段可以设置index: false,仍然保留在_source中,但不能通过普通倒排索引搜索。 -
Analyzer 主要用于文本字段
text字段需要分析;keyword字段通常作为一个完整词项索引。数值和日期使用自己的类型编码,不经过文本 Analyzer。
二、倒排索引:为什么 Mapping 和分词会直接影响查询
假设有三篇文档:
{ "id": 1, "title": "Quick brown fox" }
{ "id": 2, "title": "Quick red car" }
{ "id": 3, "title": "Brown dog" }
如果 title 使用某个 Analyzer 分析后得到:
文档 1:quick, brown, fox
文档 2:quick, red, car
文档 3:brown, dog
倒排索引可以概念化为:
quick -> 文档 1、2
brown -> 文档 1、3
fox -> 文档 1
red -> 文档 2
car -> 文档 2
dog -> 文档 3
查询 quick brown 时,Elasticsearch 不需要扫描每篇文档的完整字符串,而是读取 quick 和 brown 对应的倒排列表,再根据查询类型、布尔条件和相关性计算结果。
对 text 字段而言,至少需要考虑:
- 词项是什么;
- 词项是否区分大小写;
- 标点是否被移除;
- 一个词项在文本中的位置;
- 是否保留词项偏移量;
- 查询时是否使用与索引时相容的分析规则。
2.1 分词不是字符串替换
Analyzer 不是简单的 split(" ")。典型文本分析包含:
原始文本
↓
Character Filter:预处理字符流
↓
Tokenizer:把字符流切成初始词项
↓
Token Filter:修改、删除或增加词项
↓
最终词项流
例如,英文文本:
"The QUICK, brown foxes!"
经过一个可能的分析过程:
Tokenizer:
the
QUICK
brown
foxes
Lowercase:
the
quick
brown
foxes
Stop Filter(如果配置了停用词 the):
quick
brown
foxes
这意味着,查询 quick 是否能命中,取决于索引时是否把 QUICK 转换成了 quick。索引时和搜索时的分析规则必须在语义上匹配。
三、Mapping:字段类型决定可执行的操作
可以通过 mappings.properties 显式定义字段:
PUT products-v1
Content-Type: application/json
{
"mappings": {
"dynamic": "strict",
"properties": {
"id": {
"type": "keyword"
},
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
},
"available": {
"type": "boolean"
},
"published_at": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
}
}
}
}
这个 Mapping 表示:
id用完整值匹配,不做分词;title用于全文检索;title.keyword用于精确匹配、排序和聚合;price按两位小数缩放存储;available是布尔值;published_at接受 ISO 日期时间或毫秒时间戳;- 未声明的字段会导致写入失败,因为
dynamic为strict。
3.1 text:全文检索字段
text 适合自然语言,例如:
{
"title": "Elasticsearch Mapping Guide"
}
它通常经过 Analyzer 后,将文本拆成多个词项。适合:
match;match_phrase;- 相关性排序;
- 全文检索。
它不适合直接承担:
- 精确值聚合;
- 精确值排序;
- 作为唯一标识符。
例如:
GET products-v1/_search
Content-Type: application/json
{
"query": {
"match": {
"title": "mapping guide"
}
}
}
match 会先使用该字段的搜索 Analyzer 分析查询文本,再根据词项执行查询。它不是简单的字符串相等比较。
3.2 keyword:完整值字段
keyword 把整个输入值作为一个词项,适合:
- ID;
- 状态;
- 标签;
- 邮箱;
- SKU;
- 精确过滤;
- 排序;
- 聚合。
GET products-v1/_search
Content-Type: application/json
{
"query": {
"term": {
"id": "p-1001"
}
},
"sort": [
{
"title.keyword": "asc"
}
]
}
term 不会对查询字符串做普通文本分析。下面这个查询:
{
"term": {
"title.keyword": "Elasticsearch Mapping Guide"
}
}
要求索引中的完整 keyword 值与查询值匹配;它不会自动把大小写、空格或词序转换成全文搜索语义。
对于大小写不敏感但仍希望保持单值语义的字段,可以使用 normalizer,后文会说明。
3.3 多字段:同一输入,多个索引表示
前面的 title 定义了:
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
写入一次:
{
"title": "Elasticsearch Mapping Guide"
}
可以产生两个逻辑字段:
title -> 分析后用于全文搜索
title.keyword -> 完整字符串用于排序和聚合
查询和聚合分别使用不同字段:
GET products-v1/_search
Content-Type: application/json
{
"query": {
"match": {
"title": "mapping"
}
},
"aggs": {
"titles": {
"terms": {
"field": "title.keyword"
}
}
}
}
这是最常见的 text 与 keyword 组合。需要注意:多字段不是把一个字段动态转换成另一种类型,而是在索引阶段为同一输入建立多个字段索引结构。增加多字段或改变已有字段的定义,通常需要通过新索引重建数据。
3.4 数值类型:不要用 keyword 模拟数值
常用数值类型包括:
integer、long;float、double;scaled_float;unsigned_long等。
如果金额精度固定,例如价格精确到分,可以使用:
{
"price": {
"type": "scaled_float",
"scaling_factor": 100
}
}
写入 12.34 时,逻辑上按 1234 的缩放值处理。这里的 scaling_factor 必须符合业务精度要求;如果需要三位小数,使用 1000。它不是任意精度十进制类型,选择前应确认舍入和输入范围。
数字字段支持范围查询:
GET products-v1/_search
Content-Type: application/json
{
"query": {
"range": {
"price": {
"gte": 10,
"lt": 100
}
}
}
}
把 "12.34" 存成 keyword,虽然可以做字符串精确匹配,但不能得到正确的数值排序和范围语义。例如字符串 "100" 可能排在 "20" 之前,因为字符串比较不是数值比较。
3.5 日期类型:解析格式与内部值
date 接受配置格式定义的输入,但 Elasticsearch 内部会将日期转换为数值时间表示,用于排序和范围查询。
{
"published_at": "2025-03-08T10:00:00Z"
}
查询:
GET products-v1/_search
Content-Type: application/json
{
"query": {
"range": {
"published_at": {
"gte": "now-30d/d",
"lt": "now"
}
}
}
}
日期字符串能否写入,取决于字段的 format。写入不符合格式的值会导致解析失败,而不是自动变成缺失值。
3.6 boolean、ip、geo_point 等专用类型
字段类型应尽量表达真实语义:
{
"active": {
"type": "boolean"
},
"client_ip": {
"type": "ip"
},
"location": {
"type": "geo_point"
}
}
例如地理点可写为:
{
"location": {
"lat": 31.2304,
"lon": 121.4737
}
}
不要因为输入是字符串就统一使用 keyword。专用类型通常提供正确的校验、排序、范围或空间查询能力。
四、index、doc_values、norms 与 _source
字段类型之外,Mapping 还决定字段参与哪些索引结构。
4.1 index
{
"internal_note": {
"type": "keyword",
"index": false
}
}
index: false 表示该字段不建立用于普通查询的索引结构,但字段值仍可以保留在 _source 中。
因此下面查询无法按预期工作:
{
"query": {
"term": {
"internal_note": "secret"
}
}
}
如果业务只需要返回字段、不需要搜索,可以关闭 index。但这不是“隐藏字段”;原始值仍可能出现在 _source、快照或日志流程中,安全需求不能依赖 Mapping 设置。
4.2 doc_values
doc_values 是面向列式访问的结构,常用于:
- 排序;
- 聚合;
- 脚本访问字段值。
多数适合排序和聚合的字段默认启用。对不需要排序、聚合或脚本访问的字段,关闭它可以减少索引结构,但会牺牲对应能力。
例如:
{
"large_text": {
"type": "text",
"index": true,
"norms": false
}
}
text 默认没有用于普通排序和聚合的 doc_values。即使有 title.keyword 子字段,也应使用子字段而不是直接对 title 聚合。
4.3 norms
norms 保存与字段长度和相关性评分有关的信息。全文字段通常需要它;如果某个 text 字段只做存在性或词项匹配,不参与相关性评分,可以在创建时关闭:
{
"log_message": {
"type": "text",
"norms": false
}
}
这属于明确的能力取舍。关闭后,不能再依赖该字段的正常长度归一化相关性行为。
4.4 _source
_source 是原始文档内容的存储。它常用于:
- 返回搜索结果;
_update;- Reindex;
- 故障诊断;
- 快照恢复后的数据重建。
即使某字段 index: false,它也可能仍出现在 _source。如果使用 _source 过滤,只是控制返回内容,不等于改变索引结构。
五、Analyzer:Character Filter、Tokenizer 和 Token Filter
5.1 Character Filter
Character Filter 在 Tokenizer 之前处理字符流。典型用途包括:
- HTML 标签剥离;
- 字符映射;
- 正则替换。
例如 HTML 分析器可以先移除标签,但要注意:字符替换可能改变偏移量,Elasticsearch 会尽量维护偏移映射,以支持高亮等功能;复杂替换仍应通过 _analyze 和实际高亮结果验证。
5.2 Tokenizer
Tokenizer 将字符流拆成初始词项。常见选择包括:
standard:按 Unicode 文本分词规则处理,适合一般文本起点;whitespace:按空白切分;keyword:整个输入只生成一个词项;ngram:生成连续字符片段;edge_ngram:生成从词首开始的片段;- 语言或领域专用 Tokenizer。
Tokenizer 决定基本边界。例如 whitespace 不会像自然语言分析器那样处理复杂标点;keyword 则不会把句子拆开。
5.3 Token Filter
Token Filter 对词项流进行处理,例如:
lowercase:统一大小写;stop:移除停用词;stemmer:词干提取;synonym/synonym_graph:同义词处理;asciifolding:重音字符折叠;ngram:进一步生成片段;unique:去重。
不同 Filter 会影响精确性、相关性和索引体积。尤其是 ngram 与同义词配置,不能只看“能否命中”,还要验证结果数量和相关性。
六、索引 Analyzer 与搜索 Analyzer
一个文本字段至少有两个分析时机:
写入文档:
原文 ── index analyzer ── 倒排词项
执行查询:
查询文本 ── search analyzer ── 查询词项
可以用自定义 Analyzer:
PUT articles-v1
Content-Type: application/json
{
"settings": {
"analysis": {
"analyzer": {
"article_text": {
"type": "custom",
"char_filter": [],
"tokenizer": "standard",
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "article_text",
"search_analyzer": "article_text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
验证 Analyzer:
POST articles-v1/_analyze
Content-Type: application/json
{
"analyzer": "article_text",
"text": "The QUICK résumé"
}
典型结果会包含:
the
quick
resume
具体 Token 属性还可能包含 position、start_offset 和 end_offset。验证 Analyzer 比凭名称猜测行为可靠,因为 Tokenizer、版本和配置都会影响结果。
6.1 为什么索引和搜索分析器通常要相容
假设索引时使用:
running -> run
而搜索时不做词干提取:
running -> running
那么查询 running 可能无法命中只保存 run 的索引词项。
反过来,如果索引时产生多个变体,而查询时只产生一个原词,也可能造成召回差异。一般原则是:
- 大小写归一化通常在索引和搜索两端都保持一致;
- 词干提取、同义词等规则应明确是否用于索引、搜索或两端;
- 搜索端可以更灵活,但不能假设它能恢复索引阶段已经丢失的信息。
match 查询默认会分析查询文本;term 查询不会替你完成这种自然语言分析。因此,不能用 term 代替 match 来查询 text 字段。
6.2 search_quote_analyzer
短语查询有时需要不同的分析策略。例如普通查询允许同义词扩展,但短语查询使用同义词可能改变位置关系。可以在字段层设置 search_quote_analyzer,但应结合实际同义词规则和 match_phrase 测试,不应仅凭配置名称判断短语语义。
七、normalizer:keyword 字段的单词项归一化
keyword 不做多词分词,但可以使用 normalizer 做单词项级别的处理:
PUT users-v1
Content-Type: application/json
{
"settings": {
"analysis": {
"normalizer": {
"username_normalizer": {
"type": "custom",
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
},
"mappings": {
"properties": {
"username": {
"type": "keyword",
"normalizer": "username_normalizer"
}
}
}
}
写入:
POST users-v1/_doc/1
Content-Type: application/json
{
"username": "Jöhn"
}
查询:
GET users-v1/_search
Content-Type: application/json
{
"query": {
"term": {
"username": "john"
}
}
}
这里的关键是:
username仍是一个完整值;normalizer不会把它拆成多个词;- 索引值和
term查询值都按该字段的归一化规则处理。
normalizer 适合大小写或重音统一,不适合需要词边界的全文搜索。字段是否区分大小写、是否折叠重音,应先确定业务等价关系,因为归一化可能导致原本不同的输入在索引层变成同一个值。
八、中文分词:不要把 standard 当作中文语义分析器
中文没有以空格分隔的词边界。例如:
Elasticsearch索引设计
“索引设计”应当如何切分,取决于词典、算法和业务语料。standard 可以提供通用 Unicode 级别的处理,但不能自动等价于高质量中文分词器,也不能保证得到符合业务预期的“词”。
因此,中文搜索设计必须先回答:
- 需要按字匹配,还是按词匹配?
- 是否需要前缀搜索或联想搜索?
- 领域词,如产品名、医学术语、法律术语,是否需要自定义词典?
- 分词器来自 Elasticsearch 内置能力、官方插件还是第三方插件?
- 集群所有节点是否安装了相同插件和版本?
不要仅因为某个分词器能命中几个样例,就认为它适合生产。应通过 _analyze 检查:
POST articles-v1/_analyze
Content-Type: application/json
{
"text": "Elasticsearch索引设计与分词",
"analyzer": "standard"
}
然后使用真实业务语料覆盖:
- 专有名词;
- 连续数字;
- 中英文混排;
- 版本号;
- URL;
- 标点;
- 同义词;
- 长文本。
如果采用第三方中文分词插件,文章中的具体 Analyzer 名称、词典格式和升级兼容性取决于插件,而不是 Elasticsearch 核心 Mapping 的通用保证。插件必须在每个相关节点保持兼容安装,否则创建索引、分片分配或恢复可能失败。
8.1 字粒度与词粒度的取舍
假设文本是:
数据库索引
按字切分可能得到:
数 据 库 索 引
按词切分可能得到:
数据库 索引
字粒度通常召回更宽,但噪声可能更大;词粒度更接近自然语言,但依赖分词质量和词典。对于搜索框,常见设计是同时提供不同字段:
{
"title": {
"type": "text",
"analyzer": "content_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
是否再增加 ngram 或前缀字段,取决于查询需求。ngram 会产生更多词项,增加索引体积和查询候选数量,不能作为“中文搜索默认方案”。
九、前缀、通配符和自动补全字段的设计
全文搜索和自动补全是不同问题。
9.1 prefix 查询与 edge_ngram
如果要搜索以 elas 开头的词,索引时可以使用 edge_ngram:
PUT suggest-v1
Content-Type: application/json
{
"settings": {
"analysis": {
"analyzer": {
"prefix_index": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"prefix_filter"
]
},
"prefix_search": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase"
]
}
},
"filter": {
"prefix_filter": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 15
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "prefix_index",
"search_analyzer": "prefix_search"
}
}
}
}
输入 Elasticsearch 可能在索引中产生:
el
ela
elas
elast
...
查询 elas 只需生成 elas,即可命中对应前缀。
注意两个边界:
max_gram必须覆盖业务查询的最大前缀长度,否则更长的前缀可能无法按预期匹配;edge_ngram适合前缀,不适合任意位置包含。任意位置匹配通常需要ngram、wildcard或其他专门设计,代价不同。
9.2 wildcard 不等于全文搜索
通配符查询:
{
"query": {
"wildcard": {
"sku": "ab-*"
}
}
}
适合结构化字符串模式,不会执行普通文本 Analyzer。对任意前导通配符,如 *abc,可能需要检查大量候选,生产使用前应通过实际数据和慢查询观察成本。
十、对象、数组与 nested:避免跨对象匹配
考虑订单中的商品数组:
{
"order_id": "o-1",
"items": [
{
"sku": "A",
"color": "red"
},
{
"sku": "B",
"color": "blue"
}
]
}
如果 items 使用默认 object,字段可能被逻辑上扁平化为:
items.sku -> A、B
items.color -> red、blue
此时查询:
{
"query": {
"bool": {
"must": [
{ "term": { "items.sku": "A" } },
{ "term": { "items.color": "blue" } }
]
}
}
}
可能命中订单,但它并不表示同一个商品同时满足 sku=A 和 color=blue。这是典型的跨对象匹配。
如果必须保持数组中每个对象的字段关联,应使用 nested:
PUT orders-v1
Content-Type: application/json
{
"mappings": {
"properties": {
"order_id": {
"type": "keyword"
},
"items": {
"type": "nested",
"properties": {
"sku": {
"type": "keyword"
},
"color": {
"type": "keyword"
}
}
}
}
}
}
查询必须使用 nested:
GET orders-v1/_search
Content-Type: application/json
{
"query": {
"nested": {
"path": "items",
"query": {
"bool": {
"must": [
{ "term": { "items.sku": "A" } },
{ "term": { "items.color": "red" } }
]
}
}
}
}
}
nested 的内部机制不是给普通对象加一个标记,而是把嵌套对象作为隐藏的内部文档处理,并通过父子关系维护边界。因此:
- 查询必须指定正确的
path; - 聚合也需要使用
nested聚合; - 每个嵌套对象都会增加内部索引文档数量;
nested适合需要对象内关联的数组,不应无条件使用。
如果数据结构很深、数组很大,或需要独立更新和查询子实体,应重新评估是否拆成独立索引,而不是单纯增加嵌套层级。
10.1 flattened 的另一种取舍
当对象是动态键值集合,例如:
{
"labels": {
"env": "prod",
"team": "search",
"region": "cn"
}
}
如果每个键都建立独立字段,可能造成字段数量快速增长。flattened 可以把整个对象作为一种扁平化结构处理,适合键名不稳定、主要做简单键值检索的场景。
但它不是完整的动态 Mapping 替代品。需要精确数值范围、复杂对象关联或不同字段独立分析时,应使用明确字段或其他数据建模方式。
十一、动态映射:便利、风险与控制方式
如果没有显式 Mapping,Elasticsearch 会根据写入值尝试动态推断类型。例如:
{
"name": "Alice",
"age": 30,
"active": true,
"created_at": "2025-03-08T10:00:00Z"
}
可能被推断为:
name -> text,并带 keyword 子字段
age -> long
active -> boolean
created_at -> date(若符合动态日期检测规则)
动态推断不是业务语义理解。它可能带来:
"00123"被当成数字或日期;- 同一字段先写入字符串,后写入对象导致类型冲突;
- 用户输入的任意键不断创建字段;
- 日期字符串被错误识别;
- 一次错误写入固定了后续字段类型。
11.1 dynamic 的三种常见策略
{
"mappings": {
"dynamic": "strict",
"properties": {}
}
}
true:自动加入新字段;false:新字段不建立索引,但通常仍保留在_source;strict:遇到未声明字段时拒绝文档。
可以在对象层单独设置:
{
"mappings": {
"properties": {
"metadata": {
"type": "object",
"dynamic": false,
"properties": {}
}
}
}
}
这适合允许保留任意元数据、但不允许其无限生成索引字段的情况。
11.2 Dynamic Templates
当字段命名有稳定规则时,可以使用动态模板:
PUT logs-v1
Content-Type: application/json
{
"mappings": {
"dynamic_templates": [
{
"string_as_keyword": {
"match_mapping_type": "string",
"match": "id_*",
"mapping": {
"type": "keyword"
}
}
},
{
"text_with_keyword": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
]
}
}
模板按顺序匹配,先匹配到的模板生效。因此模板顺序是配置语义的一部分。应使用实际写入验证:
GET logs-v1/_mapping
不要只检查模板 JSON 是否能创建。
十二、Mapping 的生命周期:为什么修改后不能直接“刷新旧数据”
索引中的字段类型和索引词项结构并不是普通配置。以下变更通常不能直接对已有字段原地完成:
text改成keyword;keyword改成text;- 修改已有字段的 Analyzer;
- 修改
date格式; - 修改数值类型;
- 改变对象与
nested的关系; - 为已有文本重新生成不同词项。
原因可以按数据流推导:
- 文档写入时,旧 Analyzer 将文本转换为旧词项;
- 旧词项已经写入倒排索引;
- Mapping 变更只能改变未来写入规则,不能从旧词项反推出完整原文语义;
_source虽然保存原文,但 Elasticsearch 不会自动遍历所有旧文档并重建索引;- 因此需要新索引和重建。
常见流程是:
创建 products-v2
↓
使用新的 Mapping 和 Analyzer
↓
从 products-v1 Reindex 到 products-v2
↓
验证 Mapping、查询、聚合和数据量
↓
切换别名
↓
保留或删除旧索引
示例:
POST _reindex
Content-Type: application/json
{
"source": {
"index": "products-v1"
},
"dest": {
"index": "products-v2"
}
}
生产环境还要处理:
- 重建期间新写入如何同步;
- 是否允许重复写入;
- 失败文档如何记录;
- 切换别名时是否暂停写入;
- 新旧索引是否使用相同分片、生命周期和权限配置。
如果使用别名承载业务访问:
POST _aliases
Content-Type: application/json
{
"actions": [
{
"remove": {
"index": "products-v1",
"alias": "products"
}
},
{
"add": {
"index": "products-v2",
"alias": "products",
"is_write_index": true
}
}
]
}
别名切换是原子操作,但前提是应用始终通过别名访问,而不是把具体索引名写死。
十三、一个可运行的端到端示例
下面使用本地 Elasticsearch HTTP 接口,假定:
- Elasticsearch 已启动;
- 地址为
http://localhost:9200; - 已完成认证配置,示例省略认证头;
- 使用同一版本的 Elasticsearch 创建索引和写入文档。
13.1 创建索引和 Mapping
PUT library-v1
Content-Type: application/json
{
"settings": {
"analysis": {
"analyzer": {
"book_text": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
},
"mappings": {
"dynamic": "strict",
"properties": {
"book_id": {
"type": "keyword"
},
"title": {
"type": "text",
"analyzer": "book_text",
"search_analyzer": "book_text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"author": {
"type": "keyword"
},
"description": {
"type": "text",
"analyzer": "book_text",
"search_analyzer": "book_text"
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
},
"published_at": {
"type": "date"
}
}
}
}
这里的设计意图是:
book_id和author需要精确匹配;title需要全文搜索,也需要按完整标题聚合或排序;description只需要全文搜索;price需要数值范围查询;published_at需要日期范围和排序;dynamic: strict防止拼写错误字段悄悄进入索引。
13.2 写入文档
POST library-v1/_bulk
Content-Type: application/x-ndjson
{"index":{"_id":"b1"}}
{"book_id":"b-001","title":"Elasticsearch Mapping Guide","author":"Alice","description":"A guide to text analysis and index design.","price":39.90,"published_at":"2025-01-10T00:00:00Z"}
{"index":{"_id":"b2"}}
{"book_id":"b-002","title":"Search Systems","author":"Bob","description":"Query relevance, ranking, and aggregations.","price":49.50,"published_at":"2025-02-20T00:00:00Z"}
Bulk 请求必须使用 NDJSON:
- 每个动作和文档占一行;
- 请求体最后应有换行;
Content-Type必须是application/x-ndjson;- 响应中的顶层
errors即使为false,也应检查每个 item 的状态。
13.3 检查分析结果
POST library-v1/_analyze
Content-Type: application/json
{
"analyzer": "book_text",
"text": "The QUICK résumé"
}
可以看到小写化和重音折叠后的词项。这个 API 用来验证“实际分析结果”,特别适合诊断:
- 为什么某个词搜不到;
- 为什么短语查询位置不对;
- 为什么同义词或词干化产生意外结果;
- 为什么前缀字段产生太多词项。
13.4 全文搜索与聚合
GET library-v1/_search
Content-Type: application/json
{
"query": {
"match": {
"description": {
"query": "index analysis",
"operator": "and"
}
}
},
"aggs": {
"by_author": {
"terms": {
"field": "author"
}
}
},
"sort": [
{
"price": "asc"
}
]
}
这里有三种不同机制同时工作:
match分析index analysis,然后进行全文检索;terms聚合读取author的keyword值;sort读取price的数值列式值。
如果错误地把聚合字段写成 title,通常会收到“Fielddata is disabled on text fields by default”一类错误。这不是聚合语法错误,而是字段索引能力与操作不匹配。正确做法通常是使用 title.keyword,而不是盲目开启 fielddata。
十四、Analyzer 设计中的常见反例
14.1 只用 keyword 存标题
{
"title": {
"type": "keyword"
}
}
查询:
{
"query": {
"term": {
"title": "Elasticsearch Mapping Guide"
}
}
}
只能匹配完整值。用户搜索 mapping 时不会得到普通全文搜索语义。
适用情况:标题本身就是一个需要精确匹配的枚举或外部编码。
不适用情况:自然语言搜索框。
14.2 只用 text,然后对它聚合
{
"title": {
"type": "text"
}
}
title 会被拆成多个词项。对它做 terms 聚合意味着希望把自然语言字段当作完整标签,这两种语义冲突。
应改为:
{
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
14.3 索引时使用 edge_ngram,搜索时也使用 edge_ngram
如果索引和搜索两端都使用 edge_ngram,查询 elas 可能被再次切成:
el
ela
elas
这会改变查询语义,增加不必要的匹配条件。通常索引端使用前缀生成器,搜索端使用普通分析器:
索引端:Elasticsearch -> el, ela, elas, ...
搜索端:elas -> elas
这不是所有自动补全需求的唯一方案,但体现了索引分析器与搜索分析器的不同职责。
14.4 使用 term 查询自然语言字段
{
"query": {
"term": {
"description": "Mapping"
}
}
}
这不会像 match 那样自动完成普通文本分析。若索引时经过小写化,倒排索引中可能只有 mapping,而查询词项是 Mapping,自然无法匹配。
14.5 通过 _source 判断是否可搜索
字段出现在返回结果中,只说明它保存在 _source,不说明它建立了倒排索引。诊断可搜索性应查看:
GET library-v1/_mapping
并结合:
POST library-v1/_analyze
以及最小化查询验证,而不是只观察 _source。
十五、如何诊断 Mapping 和分词问题
15.1 先看 Mapping
GET library-v1/_mapping
重点检查:
- 字段实际类型;
- 是否是
text的多字段; - Analyzer 名称;
index是否关闭;- 对象是否为
nested; - 日期和数值格式;
- 动态模板最终是否按预期匹配。
15.2 再看 Analyzer
POST library-v1/_analyze
Content-Type: application/json
{
"field": "title",
"text": "Elasticsearch Mapping"
}
使用 field 可以验证字段实际绑定的 Analyzer。也可以显式指定:
{
"analyzer": "book_text",
"text": "Elasticsearch Mapping"
}
两者用途不同:
field检查字段配置;analyzer检查某个独立 Analyzer。
15.3 检查查询实际使用的词项
对于复杂查询,可以使用 validate API:
GET library-v1/_validate/query?explain=true
Content-Type: application/json
{
"query": {
"match": {
"title": "Mapping Guide"
}
}
}
它可以帮助判断查询是否有效,并在需要时解释查询结构。但它不是完整的相关性调试工具。相关性还涉及查询类型、字段权重、BM25 参数、词频、文档长度和其他子句。
15.4 区分“没有词项”和“没有文档”
一次未命中可能来自不同原因:
- 字段未建立索引;
- Analyzer 删除了该词;
- 查询使用了不同大小写或词形;
- 查询字段写错;
- 数据尚未刷新到可搜索状态;
nested查询缺少正确的path;term被错误用于text;- 词项存在,但布尔条件排除了文档。
排查时可以按以下顺序缩小范围:
查看 Mapping
↓
用 _analyze 看索引/查询词项
↓
用最小 match 或 term 查询验证
↓
检查 refresh、索引名称和别名
↓
检查 nested 路径和查询结构
十六、索引设计:先按查询语义建模,再决定字段类型
一个字段的设计不能只由 JSON 输入类型决定,而应由查询方式决定。
可以用下面的判断过程:
第一步:这个字段是否需要自然语言搜索?
- 是:考虑
text; - 否:不要为了“以后可能搜索”就默认使用全文分析。
第二步:是否需要完整值过滤、排序或聚合?
- 是:使用
keyword、数值、日期等适合结构化操作的类型; - 对同一自然语言字段有两种需求:使用 multi-field。
第三步:是否需要保留数组对象内部的关联?
- 是:使用
nested或重新建模; - 否:普通
object可能足够。
第四步:字段名是否稳定?
- 稳定:显式 Mapping;
- 部分动态但规则稳定:Dynamic Templates;
- 任意键值集合:考虑
flattened或不建立索引; - 不确定且高风险:
dynamic: strict或局部dynamic: false。
第五步:是否需要未来改变搜索规则?
如果 Analyzer、字段类型和索引模板可能演进,应把索引命名为版本化资源,例如:
articles-v1
articles-v2
再使用别名:
articles-read
articles-write
这样新旧索引可以并存,查询和写入切换不必依赖修改现有字段定义。
十七、字段数量、词项数量与索引成本
索引设计不仅影响查询正确性,也影响资源消耗。
17.1 多字段会增加索引结构
一个字段同时建立:
title
title.keyword
title.autocomplete
title.sort
并不是“免费增加查询入口”。每个字段可能建立独立的倒排索引、Doc Values、Norms 或其他结构。
应区分:
- 业务确实需要的查询能力;
- 只是为了测试而临时增加的字段;
- 可以在查询时处理,而不必在索引时展开的逻辑。
17.2 ngram 会扩大词项数量
如果每个词长度为 ,生成从 min_gram 到 max_gram 的所有连续片段,单个词可能产生大约:
个词项。
变量含义:
- :原始词长度;
- :当前 gram 长度;
min_gram、max_gram:生成片段的范围。
例如长度为 10 的词,若生成 2 到 5 gram:
长度 2:9 个
长度 3:8 个
长度 4:7 个
长度 5:6 个
总计:30 个
这是单个词的近似数量,真实索引还会受到重复词项、位置、文档数量和压缩结构影响。它说明了为什么不应无条件对长文本启用大范围 ngram。
17.3 Fielddata 不是默认的全文聚合方案
对 text 开启 fielddata 可以让某些操作读取分析后的词项,但可能需要在内存中构建字段数据,资源风险通常高于使用专门的 keyword 子字段。若业务需求是按原始标签或标题聚合,应在 Mapping 设计阶段增加正确的 keyword 字段,而不是把全文字段强行改造成聚合字段。
十八、与写入、ILM 和快照的边界
Mapping 和 Analyzer 决定单个索引的数据结构,但生产环境通常还要把它们放入索引生命周期中:
索引模板 / 组件模板
↓
创建新索引或数据流 backing index
↓
写入与 refresh
↓
Rollover
↓
ILM 生命周期阶段
↓
快照、删除或归档
这里有几个重要边界:
- 新的 backing index 必须继承正确的模板,否则同名字段可能出现 Mapping 冲突;
- Data Stream 的各个 backing index 应保持兼容 Mapping;
- 修改模板不会自动修改已经创建的旧索引;
- ILM 负责生命周期动作,不负责把旧索引按新 Analyzer 重新分析;
- 快照保存索引数据和元数据,但不会把错误 Mapping 自动修正;
- 升级或插件变更后,应验证自定义 Analyzer、模板和重建流程。
如果已经存在多个索引,跨索引查询要求同名字段类型相容。一个索引中的 price 是 long,另一个索引中的 price 是 keyword,查询、排序或聚合可能出现类型冲突或结果不一致。版本化索引和模板测试可以降低这种风险。
十九、规范保证、实现细节与工程建议的区别
最后需要区分三类判断。
规范保证
这些属于 Elasticsearch 对 Mapping 和查询语义的核心保证:
text字段使用配置的分析流程生成词项;keyword作为单个值索引,可配合normalizer;term查询不执行普通全文分析;nested查询维护嵌套对象边界;- 字段类型冲突会影响写入或跨索引查询;
- 改变已有字段的类型或索引语义不能通过普通 Mapping 更新完成。
常见实现
这些是通常的索引结构或默认行为,但不应替代版本文档和实际验证:
text通常使用倒排索引和 Norms;keyword、数值和日期通常使用 Doc Values 支持排序和聚合;- 动态字符串通常可能同时产生
text和keyword子字段; _source通常保存原始 JSON。
具体默认值、可用分析组件和某些参数应以所部署版本为准。
工程建议
这些需要结合业务验证,而不是 Elasticsearch 的强制规则:
- 是否使用
dynamic: strict; - 中文使用何种分词器;
- 是否增加自动补全字段;
- 是否选择
nested或拆分索引; - 是否使用
scaled_float表示金额; - 何时通过新索引重建并切换别名。
一个可靠的索引设计,应同时通过四类验证:
GET _mapping验证字段结构;_analyze验证实际词项;- 代表性查询验证召回、精确匹配、短语和聚合;
- 使用真实数据量验证索引体积、查询延迟、刷新和重建成本。
Mapping 决定“字段能做什么”,Analyzer 决定“文本以什么词项被搜索”,而索引设计负责让这两种语义在数据模型、查询方式和生命周期演进中保持一致。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:Redis 分布式协调:锁、Lua、限流、Pub/Sub 与 Streams
- 下一篇:Elasticsearch 查询与聚合:Query DSL、相关性、分页和统计
- 延伸:Elasticsearch 数据写入与运维:Bulk、Ingest、ILM、快照和升级
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论