这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 真实业务查询很少只有一个条件。本章讲解 bool 的进阶用法、constant_score、function_score、nested、父子关系、脚本查询和查询组织原则。
10.1 bool 查询再深入
GET /orders/_search
{
"query": {
"bool": {
"must": [
{ "multi_match": { "query": "笔记本", "fields": ["item_names", "title"] } }
],
"filter": [
{ "range": { "created_at": { "gte": "now-30d/d" } } },
{ "terms": { "status": ["PAID", "SHIPPED", "COMPLETED"] } }
],
"must_not": [
{ "term": { "is_deleted": true } }
],
"should": [
{ "term": { "vip_level": "HIGH" } },
{ "term": { "channel": "APP" } }
],
"minimum_should_match": 0
}
}
}
查询语义:
必须匹配商品词
且最近 30 天
且状态有效
且未删除
VIP 或 APP 命中可以加分
10.2 嵌套 bool
{
"query": {
"bool": {
"filter": [
{ "range": { "created_at": { "gte": "now-7d/d" } } },
{
"bool": {
"should": [
{ "term": { "brand": "NOVA" } },
{ "term": { "brand": "MARS" } }
],
"minimum_should_match": 1
}
}
]
}
}
}
常见错误是把“品牌 A 且价格大于 100”和“品牌 B 且价格小于 50”写成扁平 bool:
{
"bool": {
"must": [
{ "terms": { "brand": ["A", "B"] } },
{
"bool": {
"should": [
{ "range": { "price": { "gt": 100 } } },
{ "range": { "price": { "lt": 50 } } }
]
}
}
]
}
}
这个查询会错误匹配“品牌 A 价格 30”。正确方式是外层 should,内层 must:
{
"bool": {
"should": [
{
"bool": {
"must": [
{ "term": { "brand": "A" } },
{ "range": { "price": { "gt": 100 } } }
]
}
},
{
"bool": {
"must": [
{ "term": { "brand": "B" } },
{ "range": { "price": { "lt": 50 } } }
]
}
}
],
"minimum_should_match": 1
}
}
10.3 constant_score
{
"query": {
"bool": {
"must": [
{ "match": { "title": "笔记本" } }
],
"should": [
{
"constant_score": {
"filter": { "term": { "in_stock": true } },
"boost": 1.5
}
}
]
}
}
}
适合把过滤条件转换成固定加分,而不是让条件参与 BM25 文本打分。
10.4 function_score
function_score 用于把文本相关性与业务指标混合。
GET /products/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "笔记本",
"fields": ["title^3", "brand", "description"]
}
},
"functions": [
{
"filter": { "term": { "status": "ON_SALE" } },
"weight": 1.5
},
{
"field_value_factor": {
"field": "sales",
"factor": 1.2,
"modifier": "log1p",
"missing": 1
}
},
{
"gauss": {
"created_at": {
"origin": "now",
"scale": "30d",
"decay": 0.5
}
}
}
],
"score_mode": "sum",
"boost_mode": "multiply",
"min_score": 1
}
}
}
参数说明:
| 参数 | 说明 |
|---|---|
score_mode |
多个 function 得分如何合并 |
boost_mode |
查询得分与 function 得分如何合并 |
min_score |
过滤最低分 |
weight |
固定权重 |
field_value_factor |
使用字段值参与打分 |
gauss / exp / linear |
衰减函数 |
random_score |
随机打散 |
典型业务:
- 销量加成;
- 库存商品优先;
- 新品加权;
- 距离衰减;
- 赞助商品固定加权;
- 随机推荐。
注意事项:
- 权重需要持续评估;
- 过多函数会增加查询成本;
field_value_factor应使用对数修饰避免大值淹没文本分;- 赞助内容应可识别、可审计。
10.5 script_score
当内置 function 不够时,可以使用 Painless:
GET /products/_search
{
"query": {
"script_score": {
"query": {
"match": { "title": "笔记本" }
},
"script": {
"source": """
double score = _score;
if (doc['sales'].size() > 0) {
score += Math.log1p(doc['sales'].value) * params.salesWeight;
}
if (doc['in_stock'].size() > 0 && doc['in_stock'].value) {
score += params.stockWeight;
}
return score;
""",
"params": {
"salesWeight": 2.0,
"stockWeight": 1.0
}
}
}
}
}
script_score 灵活但昂贵。更复杂的精排通常在应用层或独立排序服务中完成。
10.6 nested 查询
Mapping:
PUT /products
{
"mappings": {
"properties": {
"skus": {
"type": "nested",
"properties": {
"color": { "type": "keyword" },
"size": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"stock": { "type": "integer" }
}
}
}
}
}
组合条件:
GET /products/_search
{
"query": {
"nested": {
"path": "skus",
"query": {
"bool": {
"must": [
{ "term": { "skus.color": "black" } },
{ "term": { "skus.size": "16G" } },
{ "range": { "skus.price": { "lte": 7000 } } }
]
}
},
"score_mode": "max"
}
}
}
score_mode 决定多个匹配 nested 对象的得分合并方式,常用 max、sum、avg。
10.7 nested 聚合
统计 SKU 颜色:
GET /products/_search
{
"size": 0,
"aggs": {
"skus": {
"nested": { "path": "skus" },
"aggs": {
"colors": {
"terms": { "field": "skus.color" }
}
}
}
}
}
从 nested 结果回到父文档:
{
"aggs": {
"skus": {
"nested": { "path": "skus" },
"aggs": {
"colors": {
"terms": { "field": "skus.color" },
"aggs": {
"back_to_product": {
"reverse_nested": {},
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
}
}
}
}
}
}
}
10.8 父子查询
has_child 查询父文档:
GET /company/_search
{
"query": {
"has_child": {
"type": "employee",
"query": {
"term": { "city": "Shanghai" }
},
"score_mode": "sum"
}
}
}
has_parent 查询子文档:
GET /company/_search
{
"query": {
"has_parent": {
"parent_type": "department",
"query": {
"term": { "name.keyword": "Search Platform" }
}
}
}
}
父子查询的约束:
- 父子必须在同一分片;
- 查询成本高于普通文档;
- 更新父或子都会增加索引压力;
- 大量层级不适合使用 join。
多数商品搜索场景更推荐把 SKU、类目、品牌在同步层冗余到商品文档,或为 SKU 建独立索引。
10.9 脚本查询
script query:
GET /orders/_search
{
"query": {
"bool": {
"filter": {
"script": {
"script": {
"source": "doc['amount'].value > doc['paid_amount'].value",
"lang": "painless"
}
}
}
}
}
}
脚本会绕过部分索引优化,成本高。替代方式:
- 写入时计算差额字段;
- 使用 runtime field;
- 使用 range 或 term 表达;
- 在数据库中先过滤。
10.10 查询改写实践
慢查询:
{
"query": {
"bool": {
"must": [
{ "wildcard": { "order_no": "*0001" } },
{ "match_all": {} }
]
}
}
}
优化思路:
- order_no 是否应有后缀字段;
- 是否能使用 term 精确查;
- 是否能限制时间范围;
- 是否应该放数据库;
- 是否要建补全索引。
优化后:
{
"query": {
"bool": {
"filter": [
{ "term": { "order_no_suffix": "0001" } },
{ "range": { "created_at": { "gte": "now-1d/d" } } }
]
}
}
}
10.11 查询代码封装建议
public SearchRequest buildProductSearch(ProductSearchQuery query) {
List<Query> filters = new ArrayList<>();
filters.add(termQuery("status", "ON_SALE"));
if (query.brandIds() != null) {
filters.add(termsQuery("brand_id", query.brandIds()));
}
if (query.minPrice() != null || query.maxPrice() != null) {
filters.add(rangeQuery("price", query.minPrice(), query.maxPrice()));
}
Query textQuery = query.keyword().isBlank()
? matchAll()
: multiMatch(query.keyword(), "title^3", "brand", "description");
return SearchRequest.of(r -> r
.index("products")
.from(query.from())
.size(query.size())
.query(q -> q.bool(b -> b.must(textQuery).filter(filters))));
}
原则:
- 查询构造与业务参数校验分离;
- 不允许用户直接传任意 DSL;
- 限制字段和 size;
- 输出结构化慢日志;
- 为每个查询类型建立监控。
10.12 本章小结
- bool 可以表达复杂与或非逻辑,但要注意嵌套语义;
- 不参与相关性的条件放 filter;
- function_score 用于融合文本相关性与业务指标;
- nested 保留对象数组内部关系,查询和聚合都需显式声明;
- 父子 join 成本高,优先反范式;
- 脚本查询和通配符查询要严格治理。
10.13 思考题
- 如何表达“(品牌 A 且价格>100) 或 (品牌 B 且价格<50)”?
- function_score 的 boost_mode 和 score_mode 有什么区别?
- 普通 object 为什么无法正确查询 SKU 属性组合?
- nested 聚合中 reverse_nested 的作用是什么?
- 哪些场景应该避免 script query?