Query DSL
问题
Elasticsearch 的 Query DSL 有哪些查询类型?match 和 term 有什么区别?如何构建复杂的复合查询?
答案
查询上下文 vs 过滤上下文
ES 的查询分为两种上下文,理解这个区别非常重要:
| 维度 | Query Context(查询) | Filter Context(过滤) |
|---|---|---|
| 目的 | 「这个文档有多匹配?」 | 「这个文档匹配吗?」 |
| 算分 | ✅ 计算相关性分数 | ❌ 不算分(0/1 判断) |
| 缓存 | ❌ 不缓存 | ✅ 自动缓存(Bitset) |
| 性能 | 较慢 | 更快 |
| 位置 | bool.must、bool.should | bool.filter、bool.must_not |
核心原则
- 需要按相关性排序的条件 → query context(如搜索框输入的关键词)
- 只做是/否判断的条件 → filter context(如状态、价格范围、日期)
- filter 能用就用 filter,更快且可缓存
全文查询(Full-text Queries)
全文查询会对搜索文本进行 分词 后再匹配。
match 查询
最常用的全文查询,会对搜索文本分词,然后用分词结果去匹配倒排索引:
// 基础 match
POST /products/_search
{
"query": {
"match": {
"title": "无线蓝牙耳机"
// 分词后:"无线"、"蓝牙"、"耳机"
// 默认 OR:包含任一词的文档都匹配
}
}
}
// 指定操作符
{
"query": {
"match": {
"title": {
"query": "无线蓝牙耳机",
"operator": "and" // 必须包含所有词
}
}
}
}
// minimum_should_match:至少匹配几个词
{
"query": {
"match": {
"title": {
"query": "无线蓝牙耳机降噪",
"minimum_should_match": "75%" // 至少匹配 75% 的词
}
}
}
}
match_phrase 查询
短语匹配 —— 所有词必须 按顺序相邻 出现:
{
"query": {
"match_phrase": {
"title": {
"query": "蓝牙耳机",
"slop": 1 // 允许词之间有 1 个词的间隔
}
}
}
}
multi_match 查询
在多个字段上搜索:
{
"query": {
"multi_match": {
"query": "蓝牙耳机",
"fields": ["title^3", "description"], // title 权重 ×3
"type": "best_fields" // 取最佳匹配字段的分数
}
}
}
| type | 行为 |
|---|---|
best_fields | 取匹配最好的字段的分数(默认) |
most_fields | 合并所有字段的分数 |
cross_fields | 跨字段匹配(适合人名分在多个字段) |
phrase | 在每个字段上做 match_phrase |
精确查询(Term-level Queries)
精确查询 不分词,用原始值直接匹配。
term 查询
// 精确匹配(常用于 keyword 字段)
{
"query": {
"term": {
"status": { "value": "published" }
}
}
}
// terms:匹配多个值(类似 SQL IN)
{
"query": {
"terms": {
"brand": ["Apple", "Samsung", "Huawei"]
}
}
}
不要对 text 字段用 term 查询
text 字段存储的是分词后的词项(通常小写),而 term 查询不分词。例如 title 为 "Hello World",存储的词项是 [hello, world],用 term: "Hello World" 搜不到!
- keyword 字段 → 用
term - text 字段 → 用
match
range 查询
{
"query": {
"range": {
"price": {
"gte": 100,
"lt": 500
}
}
}
}
// 日期范围
{
"query": {
"range": {
"createdAt": {
"gte": "2024-01-01",
"lt": "2024-07-01",
"format": "yyyy-MM-dd"
}
}
}
}
// 相对时间
{
"query": {
"range": {
"createdAt": {
"gte": "now-7d/d", // 7天前(向下取整到天)
"lt": "now/d"
}
}
}
}
exists 查询
// 字段存在且有值
{ "query": { "exists": { "field": "phone" } } }
wildcard 和 prefix
// 通配符(性能差,避免前缀通配)
{ "query": { "wildcard": { "title": { "value": "blue*th" } } } }
// 前缀匹配
{ "query": { "prefix": { "title.keyword": { "value": "Apple" } } } }
fuzzy 查询
// 模糊匹配(纠错),基于编辑距离
{
"query": {
"fuzzy": {
"title": {
"value": "iphome", // 拼写错误
"fuzziness": "AUTO" // 自动计算编辑距离
}
}
}
}
复合查询(Compound Queries)
bool 查询 — 最常用的复合查询
{
"query": {
"bool": {
"must": [
// 必须匹配(AND),参与算分
{ "match": { "title": "蓝牙耳机" } }
],
"should": [
// 可选匹配(OR),匹配则加分
{ "match": { "title": "降噪" } },
{ "match": { "description": "高音质" } }
],
"filter": [
// 必须匹配(AND),不算分,可缓存
{ "term": { "brand": "Sony" } },
{ "range": { "price": { "gte": 100, "lte": 1000 } } }
],
"must_not": [
// 必须不匹配(NOT),不算分
{ "term": { "status": "discontinued" } }
],
"minimum_should_match": 1 // should 中至少满足几个
}
}
}
bool 查询组合示例
等价 SQL:
SELECT * FROM products
WHERE title MATCH '蓝牙耳机' -- must (全文搜索)
AND brand = 'Sony' -- filter (精确过滤)
AND price BETWEEN 100 AND 1000 -- filter (范围过滤)
AND status != 'discontinued' -- must_not
ORDER BY _score DESC -- 按相关性排序
function_score 查询
自定义算分逻辑,比如加入「销量」「评分」等业务因素:
{
"query": {
"function_score": {
"query": { "match": { "title": "耳机" } },
"functions": [
{
"field_value_factor": {
"field": "sales", // 销量越高分数越高
"modifier": "log1p", // 取对数平滑
"factor": 0.1
}
},
{
"gauss": {
"createdAt": { // 越新的商品分数越高
"origin": "now",
"scale": "30d",
"decay": 0.5
}
}
}
],
"score_mode": "sum", // 多个函数的合并方式
"boost_mode": "multiply" // 函数分数与查询分数的合并方式
}
}
}
搜索结果处理
高亮(Highlight)
{
"query": { "match": { "title": "蓝牙耳机" } },
"highlight": {
"fields": {
"title": {
"pre_tags": ["<em>"],
"post_tags": ["</em>"],
"fragment_size": 100,
"number_of_fragments": 3
}
}
}
}
// 结果中 title 匹配的词会被 <em> 包裹
排序
{
"query": { "match": { "title": "耳机" } },
"sort": [
{ "_score": { "order": "desc" } }, // 先按相关性
{ "sales": { "order": "desc" } }, // 再按销量
{ "price": { "order": "asc" } } // 再按价格
]
}
分页
// 方式1:from + size(深分页性能差)
{ "from": 0, "size": 10 }
// 方式2:search_after(推荐翻页)
{
"size": 10,
"sort": [{ "createdAt": "desc" }, { "_id": "asc" }],
"search_after": ["2024-01-15T10:00:00Z", "abc123"] // 上一页最后一条的排序值
}
// 方式3:scroll(大数据量导出)
POST /products/_search?scroll=5m
{ "size": 1000, "query": { "match_all": {} } }
| 分页方式 | 适用场景 | 限制 |
|---|---|---|
from + size | 浅分页(前几页) | from + size ≤ 10000 |
search_after | 深分页(无限翻页) | 不能跳页 |
scroll | 全量数据导出 | 占用资源,有超时 |
_source 过滤
{
"query": { "match_all": {} },
"_source": ["title", "price", "brand"] // 只返回指定字段
}
常见面试问题
Q1: match 和 term 有什么区别?
答案:
| 维度 | match | term |
|---|---|---|
| 分词 | ✅ 对搜索文本分词 | ❌ 不分词,原值匹配 |
| 适用字段 | text 类型 | keyword 类型 |
| 用途 | 全文搜索 | 精确匹配 |
| 示例 | 搜索「蓝牙耳机」→ 分词为「蓝牙」「耳机」分别匹配 | 过滤 status = "active" |
Q2: query 和 filter 有什么区别?
答案:
- query(
must/should):计算相关性分数(_score),影响排序,不缓存 - filter(
filter/must_not):只做是否匹配的判断,不算分,结果自动缓存为 Bitset
性能优化原则:能用 filter 的条件就用 filter,只有需要按相关性排序的条件才放在 query 中。
Q3: 如何实现 MySQL 的 LIKE '%keyword%'?
答案:
ES 有多种方式:
match查询(推荐):通过分词实现,效率最高wildcard:*keyword*,类似 LIKE,性能差regexp:正则匹配,性能最差
最佳实践是使用合适的分词器 + match 查询,而不是 wildcard。
Q4: 如何处理深分页问题?
答案:
from + size 深分页的问题:ES 需要在每个分片上获取 from + size 条数据,然后在协调节点汇总排序取 Top N。如果 from 很大(如 10000),每个分片都要返回 10000+ 条数据,非常消耗内存和网络。
解决方案:
search_after:基于上一页最后一条数据的排序值翻页,不需要跳过数据,效率恒定。适合「加载更多」场景scroll:创建快照,逐批获取数据。适合数据导出场景- 业务限制:限制最大翻页深度(如最多 100 页),这也是 Google 的做法
Q5: bool 查询中 must 和 filter 的区别?
答案:
两者都要求条件必须匹配(AND 语义),区别是:
must:参与相关性评分,匹配度高的文档分数更高filter:不参与评分,只做过滤,结果可缓存
实际使用中,全文搜索条件放 must,精确过滤条件放 filter。