想象你是一家高档餐厅的老板,面对形形色色的客人:有人说“随便来点不辣的、带肉的”,有人说“我要菜单上所有包含'虾'字的菜”,还有人问“过去一个月,我们卖得最好的三道菜是什么?”如果你的服务员只会机械地念菜单,恐怕客人早就走了。但一位经验丰富的点菜员,能听懂这些混沌的需求,把客人的“人话”翻译成“后厨能执行的指令”——这正是Elasticsearch查询DSL要做的事:它是搜索引擎与用户之间的“点菜系统”,把你想找的东西变成机器能听懂的语言,再把藏在万千文档中的答案端到你面前。 本文将从“厨房点菜”的视角,带你走进ES查询DSL的核心世界:bool查询如何像“套餐组合”一样灵活拼装条件,match如何像“食材模糊识别”一样理解你的搜索意图,agg聚合如何像“营业报表”一样统计全局,nested嵌套查询如何处理“汉堡里有几层料”这类复杂结构。读完这篇文章,你将能看懂、写出并优化复杂的业务查询。

岗位关联

对AI从业者而言,ES查询DSL是大模型时代RAG(检索增强生成)的底层基石——理解了它,你才能精准地给大模型“喂”对的资料,也是AI应用中“记忆”环节的说明书。

目录
  1. 可视化演示
  2. DSL基础概念
  3. bool组合查询
  4. match全文检索
  5. agg聚合分析
  6. nested嵌套查询
  7. 性能优化技巧
  8. 实战案例:电商搜索与日志分析
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Elasticsearch查询DSL的Bool复合查询、Match全文检索流程及聚合分析与Nested嵌套查询。切换模式查看不同查询类型。

本节配有交互动效(进入视口后自动加载,约 3KB)。

DSL基础概念

想象一下,后厨里每位厨师都有自己的“暗号”系统——有的厨师喜欢说“老三样”,有的说“老位置来一份”。如果所有厨师都用不同的暗号,整个餐厅就乱了套。ES 的 DSL(Domain Specific Language,领域特定语言)就是整个后厨统一的“点菜单”格式:每个人都用同样的 JSON 结构描述自己要什么,无论是搜索引擎还是程序员,都能一眼看懂。

DSL 本质上是一套用 JSON 表达的查询语言。你不再需要像 SQL 那样写一串字符串,而是用层层嵌套的 JSON 对象来描述。比如“我想找一份不辣、带虾仁、价格在50元以下的菜”,在 DSL 里就是一层层“容器”套“容器”,像点外卖时勾选“口味偏好→不辣”“食材→虾仁”“价格区间→0-50”一样自然。

DSL 中最核心的二元结构是 Query 和 Filter。简单说,Query 是“打分制”——它不只要筛选出文档,还要算出每份文档和你的需求有多匹配(0到几分的相关度分数);而 Filter 是“淘汰制”——只分“过”与“不过”,不计算分数。这就像餐厅的“招牌菜推荐”(要按性价比、口味适配度排序)和“忌口排除”(不吃香菜直接划掉,没有中间地带)。

铺垫了这些,好比点菜员先熟悉了菜单格式:他知道“规格栏写什么表示去冰”“备注栏写什么表示免辣”。DSL 就是这份公认的“点菜单模板”,接下来我们看看最常用的几种“点法”。

score=Query匹配度(doc);Filter(doc)∈{0,1}\text{score} = \text{Query}_{\text{匹配度}}(doc); \quad \text{Filter}(doc) \in \{0, 1\}
公式通俗说明:Query 给每个文档打一个“匹配分数”,分数越高排越前;Filter 只回答“过没过”,给出 1 或 0,不参与排序。
  1. score 匹配度文档与查询条件的相关程度,由 ES 相关度算法(如 BM25)算出,分数越高越相关。
    score=BM25(query,doc)\text{score} = \text{BM25}(\text{query}, \text{doc})
  2. Filter 布尔判定Filter 条件对文档做二元判断,命中为 1,不命中为 0,不产生分数。
    Filter(doc)={1if doc∈条件集0otherwise\text{Filter}(doc) = \begin{cases} 1 & \text{if } doc \in \text{条件集} \\ 0 & \text{otherwise} \end{cases}

bool组合查询

高级点菜员最擅长的一件事,就是听懂“既要……又要……还要……”。比如:“我要宫保鸡丁,辣度可以接受,不要香菜,如果能有花生米就更好了。”这种复合需求,在 DSL 中就是 bool 查询。它像一个“需求分拣台”,把各种条件归入四个桶:

  • must:必须满足(相当于“一定要包含辣子鸡里的鸡丁”)——参与评分
  • filter:必须满足但不参与评分(相当于“不要香菜”,一刀切,不讨论好坏)
  • should:满足则加分(相当于“有花生米更好”,有了加分,没有也不排除)
  • must_not:必须不满足(相当于“绝对不能有芹菜”)

用 DSL 写出来,就像把需求贴在不同颜色的便利贴上:

四个桶各自发挥作用后,ES 把 must 和 should 的分数叠加,得到最终排序。记忆公式:must 和 should 加分数,filter 和 must_not 做减法(排除/不减分)。

想象你正在给客人推荐菜品:must 是“必须有这道菜”的铁规矩,should 是“加分项”,filter 是“忌口筛选器”。条件越多,点菜员脑子里的筛选漏斗就越精细——最终端上桌的一定是既满足硬性要求、又尽可能贴合偏好的那道菜。

scorebool=∑mustsi+∑shouldwj⋅sj−∑filter/must_not(不产生分数,仅过滤)\text{score}_{\text{bool}} = \sum_{\text{must}} s_i + \sum_{\text{should}} w_j \cdot s_j - \sum_{\text{filter/must\_not}} \text{(不产生分数,仅过滤)}
公式通俗说明:最终分数 = 每个 must 条件的分数 + 每个 should 条件的加权分数,filter 和 must_not 不贡献分数,只负责剔除不满足条件的文档。
  1. must 条件累加每个 must 子句都会产生一个匹配分数,全部加总,缺一不可。
    scoremust=∑i=1nscore(musti)\text{score}_{\text{must}} = \sum_{i=1}^{n} \text{score}(\text{must}_i)
  2. should 加权加分should 子句的分数按 boost 权重乘算后累加,匹配的 should 数越多,加分越多。
    scoreshould=∑j=1mboostj×score(shouldj)\text{score}_{\text{should}} = \sum_{j=1}^{m} \text{boost}_j \times \text{score}(\text{should}_j)
  3. filter 零分过滤filter 子句不参与求和,但它的条件必须为真,否则文档直接淘汰。
    doc保留  ⟺  ⋀k=1pfilterk(doc)=1\text{doc}_{\text{保留}} \iff \bigwedge_{k=1}^{p} \text{filter}_k(doc) = 1
json10 行
{  "query": {    "bool": {      "must": { "match": { "dish": "宫保鸡丁" } },      "filter": { "term": { "spicy": "mild" } },      "should": [ { "term": { "peanut": true } } ],      "must_not": { "term": { "coriander": true } }    }  }}

match全文检索

如果说 bool 是“配菜的规矩”,那么 match 就是“识别食材的手感”。当客人说“来点虾”时,菜单上写的是“鲜虾仁”“基围虾”“虾滑”,靠眼睛逐一比对太累——match 查询会自动把“虾”这个词分词,然后去倒排索引里查。

所谓分词,就是把一句人话拆成词条。比如“蒜蓉大虾”拆成[“蒜蓉”, “大虾”]。倒排索引则像菜谱的“食材索引表”:每种食材的旁边记着“哪些菜用到它”。比如“虾”这一行写着:菜品1、菜品3、菜品7。这样,搜索“虾”的时候,ES 不需要从头到尾翻每道菜,只需查索引表,瞬间找到所有相关菜品——这就是搜索引擎快如闪电的秘密。

match_phrase 则更严格——不仅词要出现,顺序也要对。比如搜索 “salt pepper“(盐和胡椒),普通 match 能找到”pepper and salt“(胡椒和盐),但 match_phrase 要求连续的”盐→胡椒“顺序,像点菜时强调”先放盐再放胡椒,顺序不能乱“。multi_match 则是”一道菜同时用多种配料表搜”——比如同时搜菜名和食材介绍,任何一个字段命中都算。

打个比方,match 是“大致看看这个食材像不像”,match_phrase 是“严格按顺序识别这道菜的做法”,multi_match 是“从多个维度同时判断”。掌握了这些,你的点菜速度就能从“逐页翻菜单”升级为“直接翻食材索引”。

TF-IDFt,d=TF(t,d)×IDF(t)\text{TF-IDF}_{t,d} = \text{TF}(t,d) \times \text{IDF}(t)
公式通俗说明:一个词在文档中出现越频繁(TF 高)、在整个词库中越罕见(IDF 高),这个词对这篇文档的代表性就越强。
  1. 词频 TF词条 t 在文档 d 中出现的次数,出现越多说明文档与词条越相关。
    TF(t,d)=count(t,d)total words in d\text{TF}(t, d) = \frac{\text{count}(t, d)}{\text{total words in } d}
  2. 逆文档频率 IDF词条在整个文档集合中越稀有,IDF 越大(如“虾仁”比“食物”更有区分度)。
    IDF(t)=ln⁡(Ndf(t)+1)\text{IDF}(t) = \ln\left(\frac{N}{\text{df}(t)} + 1\right)

agg聚合分析

客人吃完饭,老板问:“这个月哪道菜卖得最好?平均一份卖多少钱?”这些不是“找某道菜”,而是对一堆菜做统计——这正是聚合(agg)的用武之地。

聚合分两大类:桶聚合把文档分组(像把菜品按“川菜/粤菜/湘菜”分类),指标聚合算数值(像算每个菜系的平均售价)。两者常嵌套使用:先分桶,再在每个桶里算指标——相当于先按菜系把菜品分组,再统计每组的价格平均值。

时间直方图是另一种常见的桶聚合——把文档按时间间隔切块,如每小时、每天一个桶。它像餐厅营业报表:每小时的营收折线图,一眼看出晚市高峰是几点。

聚合的意义在于从宏观视角看数据——它不是回答“有这道菜吗”,而是回答“整体上发生了什么”。就像老板不再盯着单个菜品,而是看整张营业报表,做出“下周主推哪道菜”的决策。

avg_price桶=∑doc∈桶price(doc)∣桶∣\text{avg\_price}_{\text{桶}} = \frac{\sum_{doc \in \text{桶}} \text{price}(doc)}{|\text{桶}|}
公式通俗说明:每个桶(比如“川菜”桶)的平均价格 = 桶内所有菜价格之和 ÷ 菜的数量。
  1. 桶内求和把该桶内每道菜的价格累加。
    SUM=∑i=1npi\text{SUM} = \sum_{i=1}^{n} p_i
  2. 桶内平均总和除以桶内文档数。
    AVG=SUMn\text{AVG} = \frac{\text{SUM}}{n}
json11 行
{  "size": 0,  "aggs": {    "by_category": {      "terms": { "field": "category" },      "aggs": {        "avg_price": { "avg": { "field": "price" } }      }    }  }}

nested嵌套查询

有一类特殊的“菜”让点菜员头疼——汉堡。一个汉堡订单里有面包、牛肉饼、芝士和生菜,每种食材有自己的属性(熟度、加量、是否过敏原)。如果把这些属性当作订单的普通数组字段存储,ES 会用对象数组扁平化处理,导致“牛肉饼七分熟 + 芝士要双份”被拆散后互相错配,查“牛肉饼七分熟”时可能错误匹配到别的组合——就像客人强调“我要的汉堡是牛肉七分熟、芝士双份”,结果后厨把“牛肉全熟+芝士一份”端了上来。

ES 的 nested 类型专门解决这个问题。它把数组里的每个对象当作独立的“隐藏文档”存储,让查询在各自的“隐藏文档”内进行匹配,而不是跨对象混搭。

用这段映射和查询,就能精准找到“包含七分熟牛肉饼”的汉堡订单:

nested 是点菜员处理“套餐内多料明细”的利器。普通的数组查询会把所有食材混在一起比对,nested 则保证每个食材子项“各归其位、独立核算”——就像汉堡的每一层配料都有自己的专属标签,不会串味。

nested查询匹配条件=∃ inner_doc∈order.items:inner_doc符合所有条件\text{nested查询匹配条件} = \exists\, \text{inner\_doc} \in \text{order.items} : \text{inner\_doc符合所有条件}
公式通俗说明:nested 查询要求“同一个内层文档”同时满足所有条件,而不是不同内层文档各满足一条。
  1. 映射声明在索引映射中指定嵌套类型。
    field=type: nested\text{field} = \text{type: nested}
  2. 内部文档存在判定存在至少一个内部文档同时满足全部条件。
    ∃ i∈items:cond1(i)∧cond2(i)\exists \, i \in \text{items} : \text{cond}_1(i) \land \text{cond}_2(i)
json15 行
{  "query": {    "nested": {      "path": "items",      "query": {        "bool": {          "must": [            { "term": { "items.name": "牛肉饼" } },            { "term": { "items.doneness": "七分熟" } }          ]        }      }    }  }}

性能优化技巧

后厨再好的点菜系统,遇到“全店同时点单”也会卡顿。ES 查询同样需要讲究“出菜效率”。以下四个技巧是每个 ES 开发者都必须铭记的“后厨管理学”。

第一,filter 优先。filter 因为不算分,结果可以被 ES 缓存到内存中,下次同样的过滤条件直接命中缓存。这就像餐厅把“忌口清单”贴在墙上,不用每次重新问一遍客人。

第二,避免大通配符。wildcard 查询(如 *abc*)会强制 ES 扫描所有词条,相当于厨师为了找一道菜把整个冰箱翻个底朝天。

第三,分片策略要合理。每个分片都会独立执行查询,然后汇总结果。分片太少,单分片压力大;分片太多,汇总开销大。就像后厨团队:人数太少忙不过来,太多又互相挤占厨房。

第四,控制查询嵌套层级。过深的深层嵌套(如 bool 套 bool 套 nested 套 agg)会让执行计划膨胀。一般建议把能合并的条件并成 filter 或 should,降低执行树复杂度。

这些优化技巧,就像后厨的动线设计:食材摆放合理、流程不回头、缓存常用配料——最终客人等菜的时间更短,翻台率更高,餐厅赚得更多。

查询时间≈分片数×单分片耗时+汇总开销\text{查询时间} \approx \text{分片数} \times \text{单分片耗时} + \text{汇总开销}
公式通俗说明:总耗时约等于每个分片各跑一遍的时间加上结果合并的时间。分片太多、查询太重,总耗时都会上升。
  1. 分片并行度假设 N 个分片并行执行,理想总耗时等于单分片耗时加汇总。
    T≈T单个分片+T汇总T \approx T_{\text{单个分片}} + T_{\text{汇总}}
  2. filter 缓存命中率缓存命中时跳过算分,直接返回文档 ID。
    T有缓存≪T无缓存T_{\text{有缓存}} \ll T_{\text{无缓存}}

实战案例:电商搜索与日志分析

现在我们把所有技巧组装成一个完整场景。假设你经营一家生鲜电商 App,用户搜索“三文鱼”,你希望:找到名字含“三文鱼”的商品、排除“已下架”的、优先展示“有库存”的、再按销量排序,并同时统计所有结果的平均价格和品牌分布。

这就是一次 bool + match + agg 的组合拳,一套 DSL 一次完成“搜索 + 过滤 + 聚合”:

在日志分析场景中,nested 也能大显身手:比如每条日志包含多个性能指标(CPU、内存、IO 各自的耗时),想查“CPU 超过 80% 且持续 5 分钟”的日志,nested 保证 CPU、内存、IO 指标各归各查,不会串数据。

这一套组合,像点菜员在同一张订单上同时完成了“推荐菜品 + 过滤忌口 + 算账报数”。对一个 AI 应用工程师来说,这能力意味着:给用户检索时,既能精确锁定相关文档,又能顺带统计语料分布、辅助后续数据处理。

json16 行
{  "query": {    "bool": {      "must": [ { "match": { "name": "三文鱼" } } ],      "filter": [        { "term": { "status": "on_sale" } }      ],      "should": [ { "term": { "in_stock": true } } ]    }  },  "aggs": {    "avg_price": { "avg": { "field": "price" } },    "brands": { "terms": { "field": "brand.keyword" } }  },  "sort": [ { "sales_count": "desc" } ]}
json15 行
{  "query": {    "nested": {      "path": "metrics",      "query": {        "bool": {          "must": [            { "term": { "metrics.name": "cpu" } },            { "range": { "metrics.value": { "gte": 80 } } }          ]        }      }    }  }}

AI 应用场景

场景一:RAG系统的知识库检索

阶段:检索增强生成(RAG)中的检索召回

解决的问题:传统 RAG 只用向量相似度检索,容易召回无关文档或忽略硬性过滤条件。用 ES DSL 的 bool 组合,可以在向量检索(kNN)之外叠加 must/filter 条件,如“只召回 2023 年后的文档”“排除机密等级最高的文档”,显著提升召回精确率。

具体案例:某客服机器人知识库包含 10 万篇文档,使用 ES 同时存向量与业务字段。用户提问“退货政策”,系统先用 bool 将 match 语义匹配 + filter 限定“有效状态=published”,再配合 knn 向量召回,最终 top-5 准确率从 58% 提升到 83%。

场景二:日志智能分析辅助Agent

阶段:模型训练数据准备 / Agent 工具调用

解决的问题:大模型 Agent 要回答“最近一周哪个接口平均响应时间超过 2 秒”这类问题,无法直接用自然语言查询日志,必须把问题翻译成 ES DSL 查询。

具体案例:一个 AIOps Agent 接到用户提问后,将问题拆解为:时间范围(最近 7 天)、聚合条件(按接口分组算 avg 耗时)、过滤条件(耗时 > 2000ms)。Agent 内部生成 DSL,调用 ES 获取聚合结果,再组织成自然语言回复。

场景三:电商个性化搜索与洞察

阶段:特征工程与候选集生成

解决的问题:推荐系统需要海量候选商品及统计特征,用 ES 的 agg 聚合可以直接在查询阶段产出“价格分位数”“品牌分布”等特征,避免先全量捞数据再算。

具体案例:某电商平台为每个用户生成个性化搜索页,先用 bool 查询匹配用户偏好(match 商品描述 + filter 用户城市 + must_not 敏感品类),再用 agg 聚合按类目维度统计各价格段的商品数量,实时形成“筛选面板”。

Python 演示

python41 行
import jsonfrom elasticsearch import Elasticsearch es = Elasticsearch(["http://localhost:9200"]) INDEX = "shop_items" # 准备几条示例数据(演示用)sample_docs = [    {"name": "挪威三文鱼刺身", "category": "海鲜", "price": 88, "status": "on_sale", "in_stock": True, "brand": "渔夫码头"},    {"name": "冷冻三文鱼块", "category": "海鲜", "price": 45, "status": "on_sale", "in_stock": False, "brand": "深海优选"},    {"name": "三文鱼寿司组合", "category": "日料", "price": 120, "status": "off_sale", "in_stock": True, "brand": "寿司之魂"},    {"name": "牛肉刺身", "category": "日料", "price": 98, "status": "on_sale", "in_stock": True, "brand": "和风堂"}] def setup_index():    if es.indices.exists(index=INDEX):        es.indices.delete(index=INDEX)    mappings = {"properties": {"name": {"type": "text"}, "category": {"type": "keyword"}, "price": {"type": "float"}, "status": {"type": "keyword"}, "in_stock": {"type": "boolean"}, "brand": {"type": "keyword"}}}    es.indices.create(index=INDEX, mappings=mappings, ignore=400)    for doc in sample_docs:        es.index(index=INDEX, body=doc)    es.indices.refresh(index=INDEX) def complex_search(keyword, min_price=None, only_in_stock=False):    query = {"bool": {"must": [{"match": {"name": key    }}]}}    query["bool"]["must"] = [{"match": {"name": keyword}}]    filters = []    if min_price is not None:        filters.append({"range": {"price": {"gte": min_price}}})    if only_in_stock:        filters.append({"term": {"in_stock": True}})    if filters:        query["bool"]["filter"] = filters    body = {"query": query, "size": 5, "aggs": {"avg_price": {"avg": {"field": "price"}}, "by_category": {"terms": {"field": "category"}}}}    resp = es.search(index=INDEX, body=body)    return json.dumps(resp.body, ensure_ascii=False, indent=2) if __name__ == "__main__":    setup_index()    print(complex_search("三文鱼", min_price=50, only_in_stock=True))
目的:展示 Python 中如何用 elasticsearch 客户端执行 bool + match + agg 组合查询,模拟真实电商搜索场景。
  1. es.indices.create(index=INDEX, mappings=mappings, ignore=400)创建索引并指定字段类型映射,keyword 类型用于精确匹配和聚合
  2. query = {"bool": {"must": [{"match": {"name": key}}]}}构建 bool 查询,must 里放 match 全文检索
  3. filters.append({"term": {"in_stock": True}})用 term 精确匹配布尔字段,放入 filter 不参与评分
  4. "aggs": {"avg_price": {"avg": {"field": "price"}}}定义聚合:计算全部命中商品的平均价格
  5. "by_category": {"terms": {"field": "category"}}按商品类目分桶聚合并统计各桶数量
  6. resp = es.search(index=INDEX, body=body)真正向 ES 发起查询请求
  7. json.dumps(resp.body, ensure_ascii=False, indent=2)将响应体格式化输出,便于阅读

常见误区

误区很多初学者在 bool 查询里把所有条件都放进 must,包括状态、价格范围等无需打分的硬条件。他们觉得“must 也是必须满足,跟 filter 差不多,混着用没影响”。

正解must 和 filter 有本质区别。must 会计算相关度分数,增加排序计算量;filter 不算分、结果可被 ES 缓存。硬性过滤条件(如“状态=在售”“价格>50”)应该放 filter,既有同样的过滤效果,又大幅提升性能。好比餐厅里“忌口排除”不该由大厨重新试菜——直接按清单划掉即可,不用试吃打分。

为何常见:概念上都是“条件”,初学者容易认为“只要能过滤就行”,忽视了评分开销和缓存机制。

误区有人用 `match` 查询 `status` 字段想精确匹配“on_sale“,结果发现搜不出数据或匹配到意外的文档。他们觉得”match 是万能的,什么都能查”。

正解match 是全文检索,会先分词再匹配,适合 text 字段;term 是精确匹配,适合 keyword 或数值/日期字段。用 match 查 keyword 字段时,ES 会把“on_sale“分词成[”on”, “sale“]去倒排索引里找,很可能匹配到”on”“sale“等词,结果完全不对。想想看点菜时,”宫保鸡丁“和”宫保 鸡丁”是两种意思——精确点名要整道菜,就别让人把菜名拆开理解。

为何常见:因为 match 是教程中最常出现的查询,初学者容易把它当成默认武器,忽略了字段类型与查询类型的匹配关系。

误区初学者在索引里用普通 `object` 类型存对象数组,然后想对数组内部做多条件匹配,发现结果“不精确”。或者直接对非 nested 字段写 nested 查询,ES 直接报错。

正解必须先在映射中把字段设为 "type": "nested",ES 才会将每个数组元素作为独立文档存储和查询。如果只是 object 类型,数组会被扁平化成多个字段,不同元素的值会“串门”,导致“牛肉七分熟”和“芝士双份”被错误地匹配到一起。

为何常见:JSON 本身就支持嵌套对象,让人觉得存进去就行,没意识到 ES 内部存储时对数组的扁平化处理机制。

误区写 agg 聚合时,没有把 `size` 设为 0,结果查询返回几百条明细文档,又慢又占内存。初学者疑惑:“我只想看看聚合结果,为什么返回这么多原始数据?”

正解默认情况下 ES 会同时返回命中文档(默认每页 10 条,但设置了 size 可能更多)。如果目的是纯聚合分析,应该设置 "size": 0,让 ES 只返回聚合结果,不返回明细,节省带宽和内存。就像老板只看“营收报表”而不需要把每一笔订单小票都打印出来。

为何常见:ES 把查询和聚合放在同一个请求里,初学者以为只是“额外加了个统计功能”,忽略了返回文档的默认行为。

误区有些开发者觉得“分片越多,并行度越高,查询一定越快”,于是把一个索引分成 100 个分片。

正解分片越多,每个查询要广播到的分片数就越多,结果汇总(reducer)的开销也越大。分片数应该在创建索引时就规划好,依据数据量和单分片性能合理设定——一般为每分片 30-50GB 数据。分片太少导致单分片过大、查询慢;太多则协调节点汇总压力大。像后厨团队,10 个人做 5 道菜是高效,100 个人做 5 道菜反而互相踩脚。

为何常见:基于“并行一定更快”的直觉,忽略了分布式系统中的协调开销。

面试题

Q1. 在 ES 查询 DSL 中,filter 与 must 的核心区别是什么?

单选题 · easy

  • A.filter 一定比 must 快,因为 must 会淘汰文档
  • B.filter 不参与相关度评分,且结果可被缓存← 正确答案
  • C.must 只能放一个条件,filter 可以放多个
  • D.filter 只适用于 keyword 字段,must 只适用于 text 字段

答案:B。filter 做二元过滤、不计算相关度分数,ES 会对 filter 的结果做缓存,因此性能更好。must 则要参与打分排序。A 过于绝对,速度取决于具体情况;C 错误,must 也可以放多个条件;D 错误,两者都不限定字段类型。这就像“忌口排除”(filter)不用试菜打分,而“推荐菜”(must)需要按口味适配度排序。

Q2. match 查询对一个 text 类型字段执行“红烧肉”时,ES 内部首先做什么?

单选题 · easy

  • A.直接去文档里找完全等于“红烧肉”的字符串
  • B.对“红烧肉”进行分词,再在倒排索引中查找词条← 正确答案
  • C.把所有文档按评分从低到高排序
  • D.在正排索引中逐行扫描每个文档

答案:B。match 查询的第一步是对查询字符串执行分析器(分词),得到词条列表,再在倒排索引中逐个查找这些词条对应的文档。这相当于把“蒜蓉大虾”拆成“蒜蓉”“大虾”两个标签,再去索引表里找菜品。A 是 term 查询的行为;C 和 D 都不是 match 的匹配机制。

Q3. 以下哪种查询组合适合实现“商品名含'手机'且在售、有货优先展示”?

单选题 · mid

  • A.bool 的 must 放 match,filter 放在售,should 放有货← 正确答案
  • B.bool 全部用 must_not
  • C.match_phrase 放在 filter 中
  • D.只用 agg 聚合,不用查询

答案:A。must 放“商品名含手机”(评分),filter 放“在售”(不参与评分),should 放“有货”(命中则加分、实现优先展示)。A 完美满足了所有需求。B 逻辑相反;C 把短语查询放 filter 虽然能过滤但不具备评分能力;D 聚合不负责检索。

Q4. 关于 nested 类型,下列说法正确的是?

单选题 · mid

  • A.普通 object 类型的数组字段也支持精准多条件内部匹配
  • B.使用 nested 查询时必须先在映射中声明字段为 nested 类型← 正确答案
  • C.nested 查询比普通查询总是更快
  • D.nested 类型只适用于数值字段

答案:B。nested 类型必须显式声明,ES 才会把数组中的每个对象当作独立隐藏文档存储,从而保证同一内部文档内多条件匹配的准确性。A 错误,object 数组会被扁平化导致条件串味;C 错误,nested 通常更慢(内部文档多);D 错误,nested 常用于对象数组。

Q5. 关于 ES 性能优化,以下哪些做法是正确的?

多选题 · mid

  • A.把不需要评分的过滤条件放入 filter 子句← 正确答案
  • B.尽量避免使用 * 开头的通配符查询← 正确答案
  • C.将分片数量设置得越大越好以提升并行度
  • D.对高频过滤条件利用 filter 的结果缓存← 正确答案

答案:ABD。A 正确,filter 不评分且可缓存;B 正确,大通配符强制扫描全量词条;D 正确,filter 结果缓存可显著提速。C 错误,分片过多会增加广播和汇总开销,并非越大越好。就像后厨团队不是人越多越好,动线设计合理才重要。

Q6. 在 agg 聚合中,terms 聚合和 avg 聚合分别属于哪类?

单选题 · mid

  • A.都是指标聚合
  • B.都是桶聚合
  • C.terms 是桶聚合,avg 是指标聚合← 正确答案
  • D.terms 是指标聚合,avg 是桶聚合

答案:C。terms 按词条值把文档分组(桶聚合),avg 在桶内计算平均值(指标聚合)。两者常嵌套,形成“先分组再算指标”的模式。就像先按菜系(川菜/粤菜)把菜分组,再算每组的平均价格。

Q7. match_phrase 查询 “salt pepper“ 与 match 查询 ”salt pepper” 的关键差异是?

单选题 · hard

  • A.match_phrase 要求词条顺序一致且相邻← 正确答案
  • B.match_phrase 不做分词,直接匹配整句
  • C.match 一定比 match_phrase 召回结果少
  • D.match_phrase 不参与相关度评分

答案:A。match_phrase 要求所有词条按给定顺序出现且彼此相邻(默认 slop=0),比普通 match 更严格。普通 match 只需词条出现在文档中任意位置。B 错误,match_phrase 也会分词;C 错误,match_phrase 召回通常更少;D 错误,match_phrase 参与评分。

Q8. 以下关于 bool 查询中 should 子句的描述,正确的有哪些?

多选题 · hard

  • A.should 子句是“满足则加分,不满足则不影响”← 正确答案
  • B.在 bool 中只有 should 时,至少要匹配一个 should 条件← 正确答案
  • C.should 子句的数量越多,查询结果一定越准确
  • D.should 可以和 filter 同时存在于同一个 bool 查询中← 正确答案

答案:ABD。A 正确,should 用于提升匹配文档分数;B 正确,若 bool 中没有 must/filter 等条件,至少需满足一个 should 条件(minimum_should_match 默认为 1);D 正确,bool 中 can 同时包含 must/filter/should/must_not。C 错误,should 数量多不等于准确,可能引入噪声,需配合 min_should_match 调优。

Q9. 聚合“按小时统计订单量”应该使用哪种聚合?

单选题 · hard

  • A.terms 聚合
  • B.date_histogram 聚合← 正确答案
  • C.avg 聚合
  • D.nested 聚合

答案:B。date_histogram 按时间间隔(如 1h/1d)将文档分桶,适合时间序列统计。terms 只适合按离散词条分组;avg 是指标聚合,不分组;nested 用于嵌套对象查询。想象营业报表按小时切分,每个小时一个桶来统计订单数。

Q10. 一个 bool 查询同时包含 must、should、filter、must_not 时,最终返回的文档必须满足什么条件?

多选题 · extreme

  • A.满足所有 must 条件← 正确答案
  • B.满足所有 filter 条件← 正确答案
  • C.不满足任何 must_not 条件← 正确答案
  • D.至少满足一个 should 条件(当没有其他非 must 条件时)← 正确答案

答案:ABCD。bool 的语义组合规则:must 全部满足、filter 全部满足、must_not 一个都不能满足、should 在不存在 must/filter 时必须满足至少一个,在存在 must/filter 时满足越多分数越高(但不必需)。全部四个选项都正确。这就像点菜:主菜必须有(must)、忌口不能有(must_not)、加分项越多越好(should)、硬性过滤必须过(filter)。