同一份数据,match 能查到,term 查不到;换个字段又反过来。这不是 ES 的怪癖,而是写入时文本被分析器切成了词,查询时却按原样比较。理解这一点,match/term、text/keyword、query/filter 三组差异就一起通了。
这篇从"两个查询结果不一样"切入,再讲打分、分页、聚合与可见性,最后跑一遍最小示例。
1. 倒排索引与分析器:为什么两个查询不同
倒排索引是"词 → 文档 id 列表"的映射。写入时,文本字段先过分析器被切成词项(token),词项进索引;查询时若用 term,ES 不分析查询词,直接拿原字符串去词项表里找。
文档 title 原值 |
分析后词项 | term: "Hello World" |
match: "Hello World" |
|---|---|---|---|
Hello World |
hello、world |
不命中(没有这个词项) | 命中 |
hello-world |
hello、world |
不命中 | 命中 |
Hello |
hello(小写化) |
不命中(大小写不同) | 命中 |
match 先把查询串交给同一分析器,再逐词项求 OR(可用 operator: and),所以能命中;term 是精确词项匹配,适合 keyword、数值、日期、布尔这类不被分析的字段。
2. query 与 filter:算分与不算分
| 位置 | 是否算 _score |
是否可缓存 | 适用 |
|---|---|---|---|
query |
是 | 有限(filter cache 不参与) | 需要相关性排序的全文检索 |
filter |
否,_score 为 0 |
是 | 状态、时间范围、租户 id 等筛选 |
GET /orders/_search
{
"query": {
"bool": {
"must": [ { "match": { "remark": "退款" } } ],
"filter": [
{ "term": { "status": "PAID" } },
{ "range": { "created_at": { "gte": "now-7d/d" } } }
]
}
}
}
把 status 放进 must 不影响正确性,但每个文档多算一次分、还用不上 filter cache。过滤条件放 filter 是最基本的性能纪律。
3. text 与 keyword:同一字符串的两种存法
| 类型 | 是否分析 | 能否全文检索 | 能否排序/聚合 | 典型查询 |
|---|---|---|---|---|
text |
是,切成词项 | 能(match) |
不能(fielddata 除外) |
match、match_phrase |
keyword |
否,整体一个词项 | 只能整串匹配 | 能(doc_values) |
term、terms、sort、aggs |
常见做法是双字段:name 用 text 供搜索,name.keyword 用 keyword 供排序与聚合:
"name": {
"type": "text",
"fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
}
对 text 字段做 term 是最常见的"查不到"来源;对 text 字段直接 sort 会报 Fielddata is disabled。
4. 分析器:三段流水线
| 阶段 | 作用 | 例子 |
|---|---|---|
| character filter | 处理原始字符 | html_strip 去掉标签 |
| tokenizer | 切分成词 | standard、whitespace、ik_smart |
| token filter | 加工词项 | lowercase、stop、synonym、asciifolding |
中文默认 standard 分析器会把整句切成单字,召回高但精度差、短语查询失效。常见选择是 ik_max_word(写入,细粒度)配 ik_smart(查询,粗粒度),词典与版本按插件核对。
"analysis": {
"analyzer": {
"cn_index": { "type": "custom", "tokenizer": "ik_max_word", "filter": ["lowercase"] }
}
}
验证分析结果用 _analyze,不要靠猜:
GET /articles/_analyze
{ "field": "title", "text": "退款流程说明" }
5. bool 四子句与 minimum_should_match
| 子句 | 必须匹配 | 影响得分 | 说明 |
|---|---|---|---|
must |
是 | 是 | 参与算分的必备条件 |
filter |
是 | 否 | 必备条件,可缓存 |
should |
否 | 是 | 命中越多分越高 |
must_not |
必须不匹配 | 否 | 排除,不参与算分 |
should 在没有 must/filter 时默认至少命中一个;一旦同时出现 must 或 filter,should 变成纯加分项,全部不命中也能返回。这就是"加了 should 反而没过滤效果"的原因。
"bool": {
"must": [ { "match": { "title": "退款" } } ],
"should": [ { "term": { "vip": true } }, { "term": { "channel": "app" } } ],
"minimum_should_match": 1
}
minimum_should_match 支持 "2"、"75%"、"-1"(总数减一)等写法,按版本核对语法。
6. 排序与 _score
_score 是 similarity(默认 BM25)算出的相关性,只在 query 语境产生。一旦写了 sort,ES 不再计算 _score(除非显式 "track_scores": true),返回的 _score 为 null 或 0。
| 需求 | 写法 | 副作用 |
|---|---|---|
| 按时间倒序 | "sort": [{"created_at": "desc"}] |
相关性丢失,分页需 tiebreaker |
| 相关性优先、时间次之 | "sort": ["_score", {"created_at": "desc"}] |
保留 _score |
| 强制保留分数 | "track_scores": true |
多一次算分开销 |
排序字段必须有 doc_values(keyword、数值、日期默认有)。只按不唯一的字段排序,同值文档顺序不稳,翻页会重复或漏行,需加 _id 做 tiebreaker。
7. 分页:from/size 的上限与 search_after
from + size 的上限由 index.max_result_window 控制,默认 10000。超过就报 Result window is too large。原因是每个分片都要取出 from + size 条再归并,深翻页的成本随页码线性上升。
GET /orders/_search
{
"size": 100,
"query": { "range": { "created_at": { "gte": "now-30d/d" } } },
"sort": [ { "created_at": "desc" }, { "_id": "asc" } ],
"search_after": [ "2026-11-08T10:00:00Z", "ORD-8821" ]
}
search_after 用上一页最后一条的排序值做游标,不能跳页,适合"下一页"式遍历与导出。翻页还有 scroll(快照,适合离线导出,占资源)与 point in time + search_after(一致性更好)。选型:交互式浅翻页用 from/size,深翻页用 search_after。
8. 聚合:metric、bucket 与精度
| 类型 | 作用 | 例子 |
|---|---|---|
| metric | 算数值 | avg、sum、cardinality、percentiles |
| bucket | 分组 | terms、date_histogram、range |
| pipeline | 基于其他聚合再算 | derivative、bucket_script |
"aggs": {
"by_status": {
"terms": { "field": "status", "size": 10 },
"aggs": { "avg_amount": { "avg": { "field": "amount" } } }
}
}
terms 聚合是近似分布式的:每个分片返回自己的 Top N,协调节点再合并,所以结果可能漏掉"全局排名靠前但单分片内不靠前"的桶。响应里的 doc_count_error_upper_bound 就是误差上界,sum_other_doc_count 是被截掉的文档数。要更准就提高 size、用 shard_size,或把字段设为 keyword 后改用 composite 分页聚合。
聚合字段必须是 keyword/数值/日期;text 字段需要 .keyword 子字段,否则报 Fielddata is disabled。
9. 动态 mapping:类型一旦定下就难改
未显式定义 mapping 时,ES 按首条文档推断类型:
PUT /logs/_doc/1
{ "user_id": "1001", "created_at": "2026-11-09", "amount": 12.5 }
| 写入值 | 推断类型 | 后续问题 |
|---|---|---|
"1001"(带引号) |
text + .keyword |
想按数值范围查,只能改 mapping 重建索引 |
1001(不带引号) |
long |
之后写入 "1001" 会被强制转换或报错 |
"2026-11-09" |
date(能识别格式时) |
格式不匹配的字符串变 text |
防御方式是写模板(dynamic_templates)或关掉推断("dynamic": "strict" 让未知字段直接报错)。改字段类型不能原地改,要新建索引 + _reindex。
10. 写入可见性:写完为什么查不到
ES 写入先进内存 buffer 与 translog,默认每 1 秒 refresh 一次生成新的 segment 才可被搜索。所以"写完立刻查"可能查不到,这是 near real time 的字面含义。
| 操作 | 代价 | 用途 |
|---|---|---|
refresh=true(单次写入) |
每次生成 segment,频繁调用会拖垮 | 测试、少量关键写入 |
?refresh=wait_for |
等下一次 refresh 完成再返回 | 写后读一致性要求 |
_forcemerge |
合并 segment,重 IO | 只适合只读索引 |
批量导入时关掉 refresh("refresh_interval": "-1")、导入完再恢复,是常见做法。
11. 最小示例:从查不到到查得准
# 1) 建索引,显式 mapping,避免动态推断
curl -s -XPUT localhost:9200/articles -H 'Content-Type: application/json' -d '
{ "mappings": { "properties": {
"title": { "type": "text", "analyzer": "ik_max_word" },
"tag": { "type": "keyword" },
"views": { "type": "long" },
"created_at": { "type": "date" }
} } }'
# 2) 写入并强制可见
curl -s -XPOST 'localhost:9200/articles/_doc/1?refresh=true' -H 'Content-Type: application/json' \
-d '{"title":"退款流程说明","tag":"policy","views":120,"created_at":"2026-11-09"}'
# 3) match 命中,term 不命中
curl -s 'localhost:9200/articles/_search?q=title:退款'
curl -s 'localhost:9200/articles/_search?q=title.keyword:退款流程说明'
第 3 步的对比就是第 1 节那张表:title 被切成词项,term 找不到整串;.keyword 才是整串匹配。之后加 filter: term tag、加 terms 聚合,观察 _score 与 doc_count_error_upper_bound 的变化。
12. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
term 查 text 无结果 |
查询词未分析,与词项不相等 | 改 match 或查 .keyword |
| 写完立刻查不到 | 未到 refresh 周期 | 加 refresh=wait_for 或等待 |
Fielddata is disabled |
对 text 排序/聚合 |
用 .keyword 子字段 |
Result window is too large |
from+size 超上限 |
改 search_after |
| 聚合桶数偏少 | terms 分布式近似 + size 太小 |
提高 size/shard_size |
should 没起到过滤作用 |
同时存在 must/filter |
加 minimum_should_match |
13. 边界
| 场景 | 更合适的方案 | 理由 |
|---|---|---|
| 精确条件筛选为主 | 数据库 + 索引 | ES 的强项是全文与聚合 |
| 强事务、实时一致 | 关系库 | ES 是 NRT,无事务 |
ES 是检索引擎,不是数据库。把它当"带搜索的存储"用,问题通常出在一致性预期,而不是查询语法。
14. 自测
term查text字段为什么常常无结果?filter子句省掉了什么开销,为什么它可缓存?text与keyword分别适合哪种查询与哪种排序?should什么时候是"至少一个",什么时候只是加分?from + size深翻页的成本来自哪里,替代方案是什么?doc_count_error_upper_bound在提示什么问题?