2454 字
约 8 分钟
20
S13-B07 · Elasticsearch 查询入门:全文、过滤与聚合

同一份数据,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. 自测

  1. term 查 text 字段为什么常常无结果?
  2. filter 子句省掉了什么开销,为什么它可缓存?
  3. text 与 keyword 分别适合哪种查询与哪种排序?
  4. should 什么时候是"至少一个",什么时候只是加分?
  5. from + size 深翻页的成本来自哪里,替代方案是什么?
  6. doc_count_error_upper_bound 在提示什么问题?
S13-B07 · Elasticsearch 查询入门:全文、过滤与聚合
https://www.clxhxhhr.top/posts/6146/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。