3、ES高级查询:布尔查询(bool)、过滤上下文(filter)、评分机制(TF-IDF/BM25)、聚合分析(terms、avg、cardinality)
好,咱们今天聊点硬核的。ES 的查询,说白了就两大块:查得准不准和查得快不快。而布尔查询、过滤上下文、评分机制和聚合分析,正是解决这些问题的核心武器。我在项目中见过太多人只会用 match 和 term,一遇到复杂业务就抓瞎。嗯,今天咱们就把这些坑填上。
3.1 布尔查询(bool):组合查询的瑞士军刀
布尔查询,就是把你多个查询条件组合起来。它不像 SQL 里用 AND、OR 那么简单粗暴,而是提供了四种更精细的关系:must、should、must_not 和 filter。
我个人习惯把 bool 查询当作主查询的外壳。比如你要搜“价格低于 100 且评分高于 4.0 的电子产品”,用 bool 组合就非常清晰。
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "category": "电子产品" } }
],
"filter": [
{ "range": { "price": { "lte": 100 } } },
{ "range": { "rating": { "gte": 4.0 } } }
]
}
}
}
你想想看,must 里的条件会参与评分,而 filter 里的条件只负责过滤,不参与评分。这有什么好处?我待会讲过滤上下文时会细说。
filter 里。这样查询更快,评分也更纯粹。
3.2 过滤上下文(filter):不评分的精准打击
过滤上下文,说白了就是“只要结果,不要分数”。ES 在处理 filter 时,会做两件聪明的事:
- 不计算评分:省去了 TF-IDF 或 BM25 的计算开销。
- 缓存结果:相同的过滤条件,第二次查询直接走缓存。
我曾经在一个日志分析项目里,用 filter 替代了所有 must 里的范围查询,查询速度提升了将近 40%。为什么?因为日志查询里,时间范围这种条件根本不需要评分,你只需要“在不在这个时间段内”。
filter 和 must_not 搞混了。must_not 也是过滤,但它会检查文档是否匹配,然后排除掉。而 filter 只是保留匹配的文档。两者都不影响评分,但语义不同。
3.3 评分机制:TF-IDF 与 BM25 的较量
ES 默认的评分算法,从 5.0 版本开始就换成了 BM25。为什么?因为 TF-IDF 有个明显的缺陷:词频过高时,分数会无限增长。你想想看,一篇文章里“的”字出现 100 次,难道它就更相关吗?显然不是。
BM25 引入了两个关键参数:k1 和 b。简单说:
- k1:控制词频对评分的影响。值越大,词频影响越大。默认 1.2。
- b:控制文档长度对评分的影响。值越大,短文档越占便宜。默认 0.75。
我记得有一次做电商搜索,发现长标题的商品总是排在前面,因为关键词出现次数多。后来我把 b 调到了 0.3,短标题的商品终于有了出头之日。
PUT /my_index/_settings
{
"index": {
"similarity": {
"default": {
"type": "BM25",
"k1": 1.2,
"b": 0.75
}
}
}
}
3.4 聚合分析:从数据中挖出金矿
聚合分析,就是 ES 里的 GROUP BY。但它比 SQL 的 GROUP BY 强大得多,因为它支持嵌套、过滤、甚至脚本计算。
3.4.1 terms 聚合:分组统计
最常用的聚合,就是按某个字段的值分组,统计每个分组的文档数。比如统计每个分类下的商品数量:
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category.keyword",
"size": 10
}
}
}
}
这里有个坑:terms 聚合默认只返回前 10 个分组。如果你有 100 个分类,只看到前 10 个,很容易误判。我建议在测试时把 size 设大一点,或者用 order 排序。
3.4.2 avg 聚合:平均值计算
算平均值,比如每个分类下的平均价格:
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category.keyword",
"size": 10
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
你看,聚合可以嵌套。先按分类分组,再在每个分组里算平均价格。这种嵌套结构,是 ES 聚合的精髓。
3.4.3 cardinality 聚合:去重计数
去重计数,类似 SQL 里的 COUNT(DISTINCT)。比如统计有多少个不同的品牌:
GET /products/_search
{
"size": 0,
"aggs": {
"unique_brands": {
"cardinality": {
"field": "brand.keyword"
}
}
}
}
cardinality 聚合在数据量很大时,会消耗较多内存。ES 默认使用 HyperLogLog 算法,精度可调。如果你不需要精确值,可以设置 precision_threshold 来平衡精度和性能。
3.5 实战组合:布尔查询 + 聚合分析
最后,咱们来个综合案例。假设你要分析“价格低于 200 元的电子产品”中,每个品牌的平均评分和商品数量:
GET /products/_search
{
"query": {
"bool": {
"filter": [
{ "range": { "price": { "lte": 200 } } },
{ "term": { "category": "电子产品" } }
]
}
},
"size": 0,
"aggs": {
"by_brand": {
"terms": {
"field": "brand.keyword",
"size": 20
},
"aggs": {
"avg_rating": {
"avg": {
"field": "rating"
}
}
}
}
}
}
这个查询,先用 bool 的 filter 快速过滤出符合条件的文档,然后对结果进行聚合分析。整个过程,过滤不评分,聚合不扫描全表,效率非常高。
query 和 aggs 分开写。先调试 query 确保结果正确,再添加 aggs。这样排查问题更快。
好了,这一章的内容就到这里。布尔查询、过滤上下文、评分机制、聚合分析,这四个东西组合起来,基本能覆盖 90% 的搜索推荐场景。下一章,咱们聊聊更高级的玩法——嵌套查询与父子文档。到时候见。