跳到主要内容

Query DSL

问题

Elasticsearch 的 Query DSL 有哪些查询类型?match 和 term 有什么区别?如何构建复杂的复合查询?

答案

查询上下文 vs 过滤上下文

ES 的查询分为两种上下文,理解这个区别非常重要:

维度Query Context(查询)Filter Context(过滤)
目的「这个文档有多匹配?」「这个文档匹配吗?」
算分✅ 计算相关性分数❌ 不算分(0/1 判断)
缓存❌ 不缓存✅ 自动缓存(Bitset)
性能较慢更快
位置bool.mustbool.shouldbool.filterbool.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 有什么区别?

答案

维度matchterm
分词✅ 对搜索文本分词❌ 不分词,原值匹配
适用字段text 类型keyword 类型
用途全文搜索精确匹配
示例搜索「蓝牙耳机」→ 分词为「蓝牙」「耳机」分别匹配过滤 status = "active"

Q2: query 和 filter 有什么区别?

答案

  • querymust/should):计算相关性分数(_score),影响排序,不缓存
  • filterfilter/must_not):只做是否匹配的判断,不算分,结果自动缓存为 Bitset

性能优化原则:能用 filter 的条件就用 filter,只有需要按相关性排序的条件才放在 query 中。

Q3: 如何实现 MySQL 的 LIKE '%keyword%'?

答案

ES 有多种方式:

  1. match 查询(推荐):通过分词实现,效率最高
  2. wildcard*keyword*,类似 LIKE,性能差
  3. 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

相关链接