🔍 引言:Query DSL 是 ES 的"SQL"
如果说 Mapping 是 ES 的 Schema 设计,那 Query DSL 就是 ES 的查询语言------是你与 ES 数据打交道的日常工具。
但 Query DSL 和 SQL 有一个本质区别:SQL 的查询结果是"满足条件的数据",而 ES 的查询结果是按相关性排序的数据。理解这个区别,才能真正用好 ES 的各种查询类型。
本篇用一个真实的电商搜索场景贯穿全程:用户在搜索框输入关键词,同时可以按品牌、价格区间筛选,按销量/评分排序,看聚合统计结果(品牌分布、价格分布),最后还要支持深翻页。这些需求,我们逐一实现。
一、Query Context vs Filter Context------影响性能的关键理解
在写任何查询之前,必须先搞清楚这个核心概念,因为它直接影响查询性能和结果排序。
Query Context(查询上下文) :回答"文档有多匹配?",会计算相关性分数(_score),结果按分数排序。
Filter Context(过滤上下文) :回答"文档是否匹配?",不计算分数,结果可以缓存,性能比 Query Context 高得多。
json
{
"query": {
"bool": {
"must": [
// ↑ must 里的条件在 Query Context,会计算 _score
{ "match": { "name": "iPhone" } }
],
"filter": [
// ↑ filter 里的条件在 Filter Context,不计算 _score,走缓存
{ "term": { "brand": "Apple" } },
{ "range": { "price": { "gte": 5000 } } }
]
}
}
}
实践原则:
- 全文检索 (用户输入的关键词)→ 放
must,因为需要按相关性排序 - 精确过滤 (品牌、分类、价格区间、是否上架)→ 放
filter,性能更好
这一条规则能让查询性能提升 30-50%。
二、基础查询类型详解
2.1 match------全文检索的主力
java
// 单字段全文检索
esClient.search(s -> s
.query(q -> q
.match(m -> m
.field("name")
.query("苹果手机")
// operator: AND 表示所有词必须都出现,默认 OR
// 默认 OR 下,"苹果 手机"会匹配到只含"苹果"的文档
// AND 更严格,适合精确度要求高的场景
.operator(Operator.And)
// minimum_should_match: 至少匹配的词数/比例
// "2" 表示至少匹配2个词,"75%" 表示至少匹配75%的词
.minimumShouldMatch("2")
)
),
Product.class
);
⚠️ 踩坑 :
match的默认operator是OR,这意味着"苹果手机"会匹配到任何含有"苹果"或"手机"的文档,包括"苹果汁"、"翻盖手机"。如果你的搜索结果里出现了明显不相关的文档,很可能就是这个原因。在精确度要求高的 B 端搜索场景,建议改成AND或使用minimum_should_match。
2.2 match_phrase------短语检索
java
// 短语检索:词序必须一致,且词之间距离不超过 slop
esClient.search(s -> s
.query(q -> q
.matchPhrase(m -> m
.field("description")
.query("高清摄像头")
// slop: 允许词之间插入的其他词数量
// slop=0(默认):必须连续出现"高清摄像头"
// slop=1:允许"高清 xxx 摄像头"这样的形式
.slop(1)
)
),
Product.class
);
2.3 multi_match------多字段检索
java
// 同时在多个字段检索,是最常用的搜索入口查询
esClient.search(s -> s
.query(q -> q
.multiMatch(m -> m
.query("iPhone 15")
// 字段权重:^数字 表示权重倍数
// name 权重最高(商品名),brand 次之,description 最低
.fields("name^3", "brand^2", "description", "tags^1.5")
// type: best_fields(默认):取最高分字段的分数
// type: most_fields:多字段分数相加
// type: cross_fields:跨字段匹配,适合人名/地址等需要跨字段组合的场景
.type(TextQueryType.BestFields)
// tie_breaker: best_fields 模式下,其他字段的得分乘以这个系数加到总分上
// 0.3 是经验值,让在多个字段都匹配的文档得分更高
.tieBreaker(0.3)
)
),
Product.class
);
2.4 term/terms------精确匹配
java
// term:单值精确匹配(只能用于 keyword、number、boolean、date 类型)
// 注意:term 查询不做分析,"Apple" 和 "apple" 是不同的值
// 如果字段是 text 类型,term 通常查不到,因为 text 已经被小写化了
esClient.search(s -> s
.query(q -> q
.term(t -> t
.field("brand") // 必须是 keyword 类型
.value("Apple")
)
),
Product.class
);
// terms:多值匹配(相当于 SQL 的 IN)
esClient.search(s -> s
.query(q -> q
.terms(t -> t
.field("category")
.terms(tv -> tv.value(
List.of(
FieldValue.of("手机"),
FieldValue.of("平板"),
FieldValue.of("笔记本")
)
))
)
),
Product.class
);
2.5 range------范围查询
java
// 价格区间 + 时间范围组合
esClient.search(s -> s
.query(q -> q
.bool(b -> b
.filter(
// 价格 1000-5000
Query.of(f -> f
.range(r -> r
.field("price")
.gte(JsonData.of(1000))
.lte(JsonData.of(5000))
)
),
// 最近30天上架
Query.of(f -> f
.range(r -> r
.field("created_at")
.gte(JsonData.of("now-30d/d")) // ES 的日期表达式
.lte(JsonData.of("now/d"))
)
)
)
)
),
Product.class
);
三、bool 查询------组合查询的基石
bool 查询是 ES 中最重要的复合查询,几乎所有复杂业务查询都是 bool 的组合。
bool 查询的四个子句:
├── must → 必须匹配,影响 _score(AND + 计分)
├── should → 可选匹配,至少匹配 minimum_should_match 个(OR + 计分)
├── must_not → 必须不匹配,不影响 _score(NOT + 不计分)
└── filter → 必须匹配,不影响 _score,可缓存(AND + 不计分)
must 和 filter 都是"必须匹配",区别只在于是否计分。能用 filter 的地方不要用 must。
来看一个完整的电商搜索场景:
用户需求:搜索"高清摄像头",品牌只看"苹果"或"华为",价格 3000-8000,要有库存,排除二手商品,评分高的优先。
java
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.from(0).size(20)
.query(q -> q
.bool(b -> b
// ── must:关键词全文检索,影响相关性评分 ──────────────
.must(m -> m
.multiMatch(mm -> mm
.query("高清摄像头")
.fields("name^3", "description", "tags^2")
)
)
// ── filter:精确过滤,不影响评分,走缓存 ───────────────
// 品牌过滤(多值)
.filter(f -> f
.terms(t -> t
.field("brand")
.terms(tv -> tv.value(
List.of(FieldValue.of("苹果"), FieldValue.of("华为"))
))
)
)
// 价格范围
.filter(f -> f
.range(r -> r
.field("price")
.gte(JsonData.of(3000))
.lte(JsonData.of(8000))
)
)
// 有库存
.filter(f -> f
.range(r -> r.field("stock").gt(JsonData.of(0)))
)
// 已上架
.filter(f -> f
.term(t -> t.field("is_active").value(true))
)
// ── must_not:排除条件 ────────────────────────────────
.mustNot(mn -> mn
.term(t -> t.field("is_second_hand").value(true))
)
// ── should:非必须,但匹配则加分 ─────────────────────
// 有官方授权标识的商品加分
.should(sh -> sh
.term(t -> t.field("is_official_certified").value(true))
)
// 近期新品加分
.should(sh -> sh
.range(r -> r
.field("created_at")
.gte(JsonData.of("now-7d"))
)
)
// minimumShouldMatch: should 子句至少满足几个(这里是 0,全部可选)
// 如果 bool 里只有 should(没有 must/filter),默认至少满足 1 个
// 如果同时有 must/filter,should 默认 0 个(纯加分)
)
)
// 排序:先按相关性,再按销量
.sort(
SortOptions.of(so -> so.field(f -> f.field("_score").order(SortOrder.Desc))),
SortOptions.of(so -> so.field(f -> f.field("sales_count").order(SortOrder.Desc)))
),
Product.class
);
四、聚合(Aggregation)------ES 的统计分析能力
聚合是 ES 除搜索之外最强大的功能,可以在返回搜索结果的同时,计算统计数据------品牌分布、价格区间分布、按时间的趋势等。
4.1 Terms 聚合:品牌分布统计
java
// 同一个请求:既搜索商品,又统计品牌分布(像 SQL 的 GROUP BY)
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.size(20) // 搜索结果返回20条
.query(q -> q
.match(m -> m.field("name").query("手机"))
)
.aggregations("brand_distribution", a -> a
// terms 聚合:按 brand 字段分组,统计每组文档数
.terms(t -> t
.field("brand")
.size(10) // 只返回 Top 10 品牌
.order(NamedValue.of("_count", SortOrder.Desc)) // 按数量降序
)
// 子聚合:在每个品牌分组里,再计算平均价格
.aggregations("avg_price", aa -> aa
.avg(avg -> avg.field("price"))
)
)
// 再加一个价格区间聚合
.aggregations("price_ranges", a -> a
.range(r -> r
.field("price")
.ranges(
AggregationRange.of(ar -> ar.key("0-1000").to(1000.0)),
AggregationRange.of(ar -> ar.key("1000-3000").from(1000.0).to(3000.0)),
AggregationRange.of(ar -> ar.key("3000-5000").from(3000.0).to(5000.0)),
AggregationRange.of(ar -> ar.key("5000+").from(5000.0))
)
)
),
Product.class
);
// 解析聚合结果
StringTermsAggregate brandAgg = response.aggregations()
.get("brand_distribution")
.sterms();
List<BrandStats> brandStats = brandAgg.buckets().array().stream()
.map(bucket -> {
String brandName = bucket.key().stringValue();
long count = bucket.docCount();
double avgPrice = bucket.aggregations()
.get("avg_price")
.avg()
.value();
return new BrandStats(brandName, count, avgPrice);
})
.collect(Collectors.toList());
log.info("品牌分布: {}", brandStats);
⚠️ 踩坑 :Terms 聚合在分布式环境下,每个分片独立计算 Top N,然后汇总。这意味着结果可能不精确------某个品牌在整体上是 Top 5,但在每个分片上都排第 6,最终就会被遗漏。数据量越大、分片数越多,这个误差越明显。
_count字段旁边通常有doc_count_error_upper_bound和sum_other_doc_count字段,用于描述这个误差范围。对于精确统计要求高的场景,设置shard_size(每个分片返回更多候选)来减小误差,代价是汇总计算量增加。
4.2 Histogram 聚合:价格直方图
java
// 按价格每 500 元一个区间,生成直方图数据(用于前端展示价格分布图)
esClient.search(s -> s
.index("products")
.size(0) // 只要聚合结果,不要搜索结果(size=0 性能更好)
.aggregations("price_histogram", a -> a
.histogram(h -> h
.field("price")
.interval(500.0) // 每个区间的步长
.minDocCount(1) // 只返回文档数 >= 1 的区间(过滤空桶)
.extendedBounds(eb -> eb.min(0.0).max(20000.0)) // 固定边界
)
),
Void.class // 没有 source,用 Void
);
4.3 Nested 聚合:统计规格分布
记得第03篇的 specs 字段是 nested 类型?聚合 nested 字段需要套一层 nested 聚合:
java
// 统计"颜色"规格的分布
esClient.search(s -> s
.index("products")
.size(0)
.aggregations("color_distribution", a -> a
// 第一层:进入 nested 文档空间
.nested(n -> n.path("specs"))
.aggregations("by_spec_name", aa -> aa
// 过滤:只看 spec_name = "颜色" 的规格
.filter(f -> f.term(t -> t.field("specs.spec_name").value("颜色")))
.aggregations("color_values", aaa -> aaa
.terms(t -> t.field("specs.spec_value").size(20))
)
)
),
Void.class
);
五、相关性打分干预------function_score
默认情况下,ES 按 BM25 算法打分:关键词在文档中出现频率越高、文档越短,得分越高。但在实际业务中,我们往往需要把业务因素 (销量、评分、上架时间、是否促销)融入排序。这就是 function_score 的用武之地。
5.1 基础用法:线性衰减
java
// 新品优先:上架时间越近,得分越高(时间衰减函数)
esClient.search(s -> s
.query(q -> q
.functionScore(fs -> fs
// 原始查询(基础相关性)
.query(inner -> inner
.match(m -> m.field("name").query("手机"))
)
// 衰减函数:距今越远,分数乘以越小的系数
.functions(f -> f
.gauss(g -> g
.field("created_at")
.placement(p -> p
.origin("now") // 以当前时间为中心
.scale("7d") // 7天内分数衰减到 0.5
.decay(0.5) // 衰减系数
.offset("1d") // 1天内不衰减
)
)
)
// boost_mode: 如何把 function 的结果与原始 _score 结合
// multiply: 相乘(默认),sum: 相加,replace: 直接用 function 得分
.boostMode(FunctionBoostMode.Multiply)
)
),
Product.class
);
5.2 多函数组合:综合排序
这是实际电商搜索中最常用的模式:把多个因素综合起来影响排序。
java
// 综合排序:相关性 × 销量权重 × 评分权重 × 新品加成
esClient.search(s -> s
.query(q -> q
.functionScore(fs -> fs
.query(inner -> inner
.multiMatch(m -> m
.query("手机")
.fields("name^3", "description")
)
)
.functions(
// 函数1:按销量加权(sales_count 字段值作为权重因子)
FunctionScore.of(f -> f
.fieldValueFactor(fvf -> fvf
.field("sales_count")
.factor(0.001) // 缩放系数,防止销量数值太大压制相关性
.modifier(FieldValueFactorModifier.Log1p) // log(1+x),平滑大值
.missing(1.0) // 字段缺失时的默认值
)
),
// 函数2:按评分加权
FunctionScore.of(f -> f
.fieldValueFactor(fvf -> fvf
.field("rating")
.factor(2.0)
.modifier(FieldValueFactorModifier.None)
.missing(3.0)
)
),
// 函数3:促销商品额外加 boost
FunctionScore.of(f -> f
.filter(filter -> filter
.term(t -> t.field("is_promotion").value(true))
)
.weight(1.5) // 促销商品得分 × 1.5
)
)
// score_mode: 多个 function 的结果如何合并
// multiply: 相乘,sum: 相加,avg: 平均,first: 取第一个匹配的
.scoreMode(FunctionScoreMode.Multiply)
.boostMode(FunctionBoostMode.Sum) // function 结果与 _score 相加
.maxBoost(3.0) // 限制最大 boost 倍数,防止某个因素把结果完全主导
)
),
Product.class
);
⚠️ 踩坑 :
script_score是 function_score 的一种,允许用 Painless 脚本写任意打分逻辑,非常灵活。但每次查询都要对每个命中文档执行脚本,当结果集很大时(几万+命中),性能会急剧下降。建议优先用field_value_factor和内置衰减函数,只在不得不用时才用script_score,并且要配合pre_filter_shard_size减少脚本执行次数。
六、高亮(Highlight)实现
高亮是搜索结果中关键词飘红的效果,ES 原生支持,但有几个细节需要注意:
java
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q.match(m -> m.field("name").query("iPhone 摄像头")))
.highlight(h -> h
// 全局高亮配置
.preTags("<em class='es-highlight'>")
.postTags("</em>")
// 每个字段可以单独覆盖全局配置
.fields("name", hf -> hf
// numberOfFragments=0:返回整个字段内容(不截断)
// 适合标题类短字段
.numberOfFragments(0)
)
.fields("description", hf -> hf
// 长文本截取3个片段,每段150字
.numberOfFragments(3)
.fragmentSize(150)
// order: score 表示按高亮关键词数量排序片段(最相关的在前)
.order(HighlighterOrder.Score)
)
),
Product.class
);
// 从响应中提取高亮内容
response.hits().hits().forEach(hit -> {
Product product = hit.source();
Map<String, List<String>> highlights = hit.highlight();
// 如果有高亮,用高亮内容替换原始内容(前端直接渲染)
String displayName = highlights.containsKey("name")
? highlights.get("name").get(0)
: product.getName();
List<String> descFragments = highlights.getOrDefault("description", List.of());
log.info("商品: {}, 描述摘要: {}", displayName, descFragments);
});
⚠️ 踩坑 :高亮使用的分析器必须和查询时一致,否则高亮会失效(找不到 token 的位置)。如果你在查询时用了
ik_smart,高亮配置里也要保持一致。另外,对于nested类型的字段,高亮需要额外配置nested_path,否则高亮内容是空的。
七、分页方案对比与选择
分页是搜索功能的必选项,但 ES 的三种分页方案各有适用场景,选错了会踩大坑。
7.1 from/size------传统分页
java
// 最常见的分页,类似 SQL 的 OFFSET LIMIT
esClient.search(s -> s
.from(page * size) // 跳过前 N 条
.size(size) // 返回 N 条
// ...
);
限制 :from + size 不能超过 index.max_result_window(默认 10000)。超过时报错 Result window is too large。
为什么有这个限制?因为 ES 的分布式查询是 scatter-gather 模式:每个分片返回 from + size 条结果,Coordinating Node 汇总后取前 size 条。如果 from=9000, size=10,每个分片要返回 9010 条数据到 Coordinating Node,3 个分片就是 27030 条,内存和网络开销极大。
⚠️ 踩坑 :很多人遇到
max_result_window限制后,第一反应是把这个值改大(比如改到 100000)。这是饮鸩止渴------数据量越大,深翻页越慢,最终会导致查询超时甚至 OOM。正确方案是用search_after。
7.2 search_after------深翻页的正确姿势
search_after 是游标分页,每次请求返回当前页最后一条数据的排序值,下次请求从这个值之后开始取:
java
// 第一页:正常查询,获取第一页数据
SearchResponse<Product> firstPage = esClient.search(s -> s
.index("products")
.size(20)
.query(q -> q.match(m -> m.field("name").query("手机")))
.sort(
// 必须有稳定的排序,且最后一个排序字段必须唯一(通常用 _id)
SortOptions.of(so -> so.field(f -> f.field("_score").order(SortOrder.Desc))),
SortOptions.of(so -> so.field(f -> f.field("sales_count").order(SortOrder.Desc))),
SortOptions.of(so -> so.field(f -> f.field("_id").order(SortOrder.Asc)))
),
Product.class
);
// 获取最后一条的排序值,用于下一页查询
List<FieldValue> lastSortValues = firstPage.hits().hits()
.get(firstPage.hits().hits().size() - 1)
.sort();
// 第二页:使用 searchAfter
SearchResponse<Product> secondPage = esClient.search(s -> s
.index("products")
.size(20)
.query(q -> q.match(m -> m.field("name").query("手机")))
.sort(/* 同第一页的排序 */)
.searchAfter(lastSortValues), // 关键:传入上一页最后一条的排序值
Product.class
);
search_after 的优点是无论翻到第几页,性能都是稳定的(每次只取 size 条)。缺点是不支持跳页,只能前后翻,适合"加载更多"或移动端的上拉刷新场景。
7.3 Point In Time(PIT)+ search_after------兼顾一致性
search_after 有一个隐患:翻页过程中,如果有新文档写入或旧文档删除,可能导致数据错位(某条数据被跳过或重复出现)。PIT(时间点快照)解决了这个问题:
java
// 1. 创建 PIT 快照(有效期10分钟)
OpenPointInTimeResponse pitResponse = esClient.openPointInTime(p -> p
.index("products")
.keepAlive(Time.of(t -> t.time("10m")))
);
String pitId = pitResponse.id();
// 2. 使用 PIT 分页(数据集固定在创建 PIT 时的快照)
SearchResponse<Product> response = esClient.search(s -> s
.pit(p -> p.id(pitId).keepAlive(Time.of(t -> t.time("10m"))))
.size(20)
.sort(SortOptions.of(so -> so.field(f -> f.field("_shard_doc").order(SortOrder.Asc))))
.searchAfter(lastSortValues),
Product.class
);
// 3. 用完后关闭 PIT,释放服务端资源
esClient.closePointInTime(c -> c.id(pitId));
7.4 三种方案对比
| 方案 | 跳页 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|---|
| from/size | ✅ | 低(无快照) | 深翻页性能差 | 数据量小,精确跳页 |
| search_after | ❌ | 中(无快照) | 稳定 O(size) | 加载更多、移动端 |
| PIT + search_after | ❌ | 高(快照) | 稳定 O(size) | 高一致性分页需求 |
八、Java 完整搜索服务封装
把前面所有知识点整合成一个生产可用的搜索服务:
java
// ProductSearchService.java(完整版)
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductSearchService {
private final ElasticsearchClient esClient;
/**
* 电商商品搜索:综合了全文检索、多维过滤、聚合统计、高亮、分页
*
* 设计原则:
* 1. 关键词检索放 must,影响相关性排序
* 2. 所有过滤条件放 filter,走缓存提升性能
* 3. function_score 融入业务权重(销量、评分)
* 4. 聚合与搜索同请求完成,减少网络往返
*/
public ProductSearchResponse search(ProductSearchRequest req) {
try {
SearchResponse<Product> esResponse = esClient.search(s -> s
.index(req.getTenantIndexName()) // SaaS:按租户路由到不同索引
.from(req.getPage() * req.getSize())
.size(req.getSize())
.query(buildQuery(req))
.highlight(buildHighlight())
.aggregations("brands", a -> a.terms(t -> t.field("brand").size(20)))
.aggregations("price_ranges", a -> a.range(r -> r
.field("price")
.ranges(
AggregationRange.of(ar -> ar.key("0-999").to(1000.0)),
AggregationRange.of(ar -> ar.key("1000-2999").from(1000.0).to(3000.0)),
AggregationRange.of(ar -> ar.key("3000+").from(3000.0))
)
))
.sort(buildSort(req)),
Product.class
);
return ProductSearchResponse.builder()
.total(esResponse.hits().total().value())
.products(parseHits(esResponse))
.brandFacets(parseBrandFacets(esResponse))
.priceRangeFacets(parsePriceRangeFacets(esResponse))
.build();
} catch (IOException e) {
log.error("ES 查询异常", e);
throw new SearchException("搜索服务暂时不可用", e);
}
}
private Query buildQuery(ProductSearchRequest req) {
return Query.of(q -> q
.functionScore(fs -> fs
.query(inner -> inner
.bool(b -> {
// 全文检索
if (StringUtils.hasText(req.getKeyword())) {
b.must(m -> m.multiMatch(mm -> mm
.query(req.getKeyword())
.fields("name^3", "brand^2", "description", "tags^1.5")
));
} else {
b.must(m -> m.matchAll(ma -> ma));
}
// SaaS 租户隔离(必须带这个过滤)
b.filter(f -> f.term(t -> t.field("tenant_id").value(req.getTenantId())));
// 品牌过滤
if (!CollectionUtils.isEmpty(req.getBrands())) {
b.filter(f -> f.terms(t -> t.field("brand")
.terms(tv -> tv.value(req.getBrands().stream()
.map(FieldValue::of)
.collect(Collectors.toList())))));
}
// 价格范围
if (req.getMinPrice() != null || req.getMaxPrice() != null) {
b.filter(f -> f.range(r -> {
r.field("price");
if (req.getMinPrice() != null) r.gte(JsonData.of(req.getMinPrice()));
if (req.getMaxPrice() != null) r.lte(JsonData.of(req.getMaxPrice()));
return r;
}));
}
b.filter(f -> f.term(t -> t.field("is_active").value(true)));
return b;
})
)
// 销量和评分加权
.functions(
FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf
.field("sales_count").factor(0.0001).modifier(FieldValueFactorModifier.Log1p).missing(1.0)
)),
FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf
.field("rating").factor(1.0).modifier(FieldValueFactorModifier.None).missing(3.0)
))
)
.scoreMode(FunctionScoreMode.Sum)
.boostMode(FunctionBoostMode.Multiply)
)
);
}
private HighlightBase buildHighlight() {
return HighlightBase.of(h -> h
.preTags("<em>").postTags("</em>")
.fields("name", f -> f.numberOfFragments(0))
.fields("description", f -> f.numberOfFragments(2).fragmentSize(120))
);
}
private List<SortOptions> buildSort(ProductSearchRequest req) {
// 按用户选择的排序方式
return switch (req.getSortBy()) {
case "price_asc" -> List.of(
SortOptions.of(so -> so.field(f -> f.field("price").order(SortOrder.Asc)))
);
case "price_desc" -> List.of(
SortOptions.of(so -> so.field(f -> f.field("price").order(SortOrder.Desc)))
);
case "sales" -> List.of(
SortOptions.of(so -> so.field(f -> f.field("sales_count").order(SortOrder.Desc)))
);
default -> List.of(
SortOptions.of(so -> so.field(f -> f.field("_score").order(SortOrder.Desc))),
SortOptions.of(so -> so.field(f -> f.field("sales_count").order(SortOrder.Desc)))
);
};
}
}
❓ 高频面试 & AI 问答
Q: ES 的 query 和 filter 有什么区别?
A: query 在 Query Context 下执行,会计算相关性分数(_score),结果按分数排序,不走缓存;filter 在 Filter Context 下执行,只判断文档是否满足条件,不计算分数,结果可以被缓存,性能更好。实践中,全文检索放 query,精确过滤条件放 filter。
Q: bool 查询的 must 和 filter 都是"必须匹配",区别是什么?
A: must 会参与相关性分数计算(影响 _score),filter 不参与计分且结果可缓存。对于精确过滤条件(品牌、分类、价格区间),放 filter 性能更好;对于全文检索(需要按相关性排序),放 must。
Q: ES 深翻页为什么性能差?如何解决?
A: ES 用 scatter-gather 模式分页,每个分片都需要返回 from+size 条数据,from 越大,每个分片返回的数据越多,网络和内存开销呈线性增长。解决方案是使用 search_after 游标分页,或结合 Point In Time 保证翻页一致性。
Q: function_score 和直接对结果排序(sort)有什么区别?
A: sort 完全忽略相关性分数,按指定字段绝对排序;function_score 是在相关性分数基础上做权重干预,保留了"关键词相关性"的影响,同时融入业务因素。搜索场景通常用 function_score,纯列表浏览场景用 sort。
🔗 上一篇
第03篇 :深入 Mapping 与数据类型设计------ES Schema 设计避坑指南
🔗 下一篇
第05篇 :分词器深度定制 + 中文搜索优化------让搜索真正"懂"中文
为什么"华为"搜不到"华为 Mate 60"?为什么搜"shouji"找不到"手机"?中文分词器的坑,比英文多得多。下篇从 IK 分词器安装开始,到自定义词典、拼音搜索、同义词扩展,再到搜索建议(自动补全 + 拼写纠错),全部实战演示。