聚合前必须先用filter缩小文档集,因es聚合默认在全部匹配文档上运行,未过滤会导致统计失真;filter中条件不打分、可缓存,性能优于must;全文检索用must,过滤逻辑严格放在filter;terms聚合字段须为keyword类型,text字段需通过fields多字段映射支持聚合与检索;嵌套聚合需显式声明nested路径;laravel中需白名单校验用户输入参数并限制数值范围。

聚合前必须先用 filter 缩小文档集
ES 的聚合(aggs)默认在 query 返回的所有匹配文档上运行,但如果你只关心“已上架且价格 ≤ 5000 的商品”的类目分布,却没提前过滤,聚合结果就会包含下架或高价商品,统计失真。关键点在于:聚合本身不自动过滤,它只“数”你给它的数据。
正确做法是把确定性条件(如状态、范围、精确值)全塞进 bool.filter,再在其外层定义 aggs:
{
"query": {
"bool": {
"filter": [
{"term": {"sale": true}},
{"range": {"price": {"lte": 5000}}}
]
}
},
"aggs": {
"by_category": {
"terms": {"field": "category_id"}
}
}
}
-
filter中的条件不参与相关性打分,可被缓存,性能远高于写在must里 - 别把
match或multi_match放 filter——它们是查询语句,不是过滤器 - 如果要用全文检索+聚合,必须把检索逻辑放
must,过滤逻辑严格留在filter,两者共存于同一bool
terms 聚合字段必须是 keyword 类型
对 category_id 或 brand_name 做 terms 聚合时,如果字段在 mapping 中是 text 类型(默认会分词),ES 会按分词后碎片聚合,比如 “iPhone 15 Pro” 变成 [“iphone”, “15”, “pro”],结果出现大量无意义单字桶。必须确认该字段类型为 keyword:
PUT /products/_mapping
{
"properties": {
"category_id": {"type": "keyword"},
"brand_name": {"type": "keyword"}
}
}
- 已有索引无法直接改字段类型,需 reindex;新索引建模阶段就要定死
- 若字段同时要支持全文检索和聚合,用
fields多字段映射:"type": "text", "fields": {"keyword": {"type": "keyword"}},聚合时查brand_name.keyword - 别依赖
fielddata: true强开 text 字段聚合——内存爆炸风险极高,9.x 已默认禁用
嵌套聚合需配合 inner_hits 或 reverse_nested
当商品有 skus 嵌套对象(含库存、颜色等),你想统计“每个颜色下的平均库存”,就不能直接对 skus.color 做 terms 聚合——嵌套字段必须显式声明路径并启用 nested 查询上下文。
正确结构:
{
"query": {
"nested": {
"path": "skus",
"query": {
"bool": {
"filter": [
{"term": {"skus.in_stock": true}}
]
}
}
}
},
"aggs": {
"by_color": {
"nested": {"path": "skus"},
"aggs": {
"colors": {"terms": {"field": "skus.color"}},
"avg_stock": {"avg": {"field": "skus.stock"}}
}
}
}
}
- 外层
nestedquery 确保只命中有符合条件 sku 的商品文档 - 内层
nestedaggs 指向同一 path,才能正确遍历嵌套对象 - 漏掉任意一个
nested声明,ES 会报错或返回空桶
Laravel 中调用原生 client 传参要防注入
用户输入的类目 ID、价格区间等参数如果直接拼进 DSL body,可能触发恶意查询(如通过 script 或 regexp 注入)。Laravel 项目中不能依赖 Scout 的抽象层来“自动防护”——Scout 不校验原始 DSL。
必须手动白名单校验:
- 价格范围只接受整型:
filter_var($request->input('min_price'), FILTER_VALIDATE_INT) - 排序字段限定数组:
in_array($sort, ['price', 'sales_count', 'created_at'], true) - 类目 ID 必须匹配正则
/^\d+(-\d+)*$/(适配 category_path 树形结构) - 聚合字段名从固定配置读取,不接受前端传入的任意
field
最易被忽略的是:即使用了 filter,如果用户能控制 range 的 gte/lte 值,仍可能构造超大范围拖垮集群——务必加数值上下限(如 price ≤ 100000)。











