Elasticsearch 查询 DSL 深入——为什么你的搜索总是不准 / Elasticsearch Query DSL Deep Dive
📅 创建时间:2026-07-28 🏷️ 标签:#Elasticsearch #QueryDSL #全文搜索 #聚合 #BM25 #相关性调优 📚 前置知识:[[./00-overview]](搜索引擎概览) [[/03-web/07-middleware/06-elasticsearch]](ES 倒排索引基础)
📋 本章目标
- 理解 Query Context 与 Filter Context 的本质区别(score vs no-score)
- 掌握核心全文查询:match、multi_match、match_phrase、query_string
- 掌握复合查询:bool、boosting、dis_max 的组合策略
- 理解 Aggregations 的三种类型:Bucket / Metrics / Pipeline
- 能够调优相关性评分:BM25 参数、boosting、function_score、script_score
- 掌握六种常见搜索模式的实现:跨字段、模糊、自动补全、地理位置、嵌套、高亮
- 学会诊断查询性能:Profile API、慢查询日志、filter 缓存、routing
第1部分:Query Context vs Filter Context
1.1 两种上下文的本质区别
在 ES 中,每一次查询操作都在两种"上下文"中选择:这个条件是用来打分(Query Context)还是用来筛选(Filter Context)?这是理解 ES 查询性能的第一步。
┌─────────────────────────────────────────────────────────────┐
│ Query Context vs Filter Context │
├─────────────────────────────────────────────────────────────┤
│ │
│ Query Context(查询上下文): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "这个文档和用户的查询有多匹配?" │ │
│ │ │ │
│ │ • 会计算相关性得分 (_score) │ │
│ │ • 得分影响排序 │ │
│ │ • 不缓存(每次都重新算分) │ │
│ │ • 举例:"标题里包含'小米手机'的文档" │ │
│ │ → 完全匹配的得分高,部分匹配的得分低 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Filter Context(过滤上下文): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "这个文档符合条件吗?是或否" │ │
│ │ │ │
│ │ • 不计算评分(_score 为 0 或忽略) │ │
│ │ • 只做二选一的过滤 │ │
│ │ • 自动缓存(ES 内置 filter cache) │ │
│ │ • 举例:"价格在 1000-5000 之间" │ │
│ │ → 1001 元和 4999 元没有区别,都是"符合" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 核心原则: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ✅ 全文匹配 → Query Context(match) │ │
│ │ ✅ 精确筛选 → Filter Context(term, range) │ │
│ │ ✅ 两者组合 → bool 查询(must + filter) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.2 实践:同一个条件的不同写法
┌─────────────────────────────────────────────────────────────┐
│ 同一条件的三种写法及其行为 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 场景:查找 category 为 "phone" 且标题包含 "小米" 的商品 │
│ │
│ 写法1:全部用 Query Context(不推荐) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "query": { │ │
│ │ "bool": { │ │
│ │ "must": [ │ │
│ │ {"match": {"category": "phone"}}, // ← Q │ │
│ │ {"match": {"title": "小米"}} // ← Q │ │
│ │ ] │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ │ 问题:category 精确匹配不需要打分,浪费 CPU │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 写法2:Query + Filter(推荐) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "query": { │ │
│ │ "bool": { │ │
│ │ "must": [ │ │
│ │ {"match": {"title": "小米"}} // Q │ │
│ │ ], │ │
│ │ "filter": [ │ │
│ │ {"term": {"category": "phone"}} // F │ │
│ │ ] │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ │ 优点:filter 条件自动缓存,不参与评分 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 写法3:post_filter(特定场景) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "query": {"match": {"title": "小米"}}, │ │
│ │ "post_filter": {"term": {"category": "phone"}} │ │
│ │ } │ │
│ │ 注意:post_filter 在聚合计算之后才过滤 │ │
│ │ 适用:需要"全量聚合 + 部分过滤结果的聚合"时 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:全文查询
2.1 match —— 最常用的全文查询
┌─────────────────────────────────────────────────────────────┐
│ match 查询详解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ match 查询会自动分词,然后对每个词做 term 查询后取 OR │
│ │
│ 示例: │
│ {"match": {"title": "小米手机"}} │
│ ↓ 分词 (ik_max_word) │
│ [小米, 手机] │
│ ↓ 转为 Lucene 查询 │
│ title:小米 OR title:手机 │
│ │
│ 控制匹配精度:operator 参数 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ {"match": { │ │
│ │ "title": { │ │
│ │ "query": "小米手机", │ │
│ │ "operator": "and" // 改为 AND │ │
│ │ } │ │
│ │ }} │ │
│ │ → title:小米 AND title:手机 │ │
│ │ → 必须同时包含"小米"和"手机" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 控制模糊度:minimum_should_match │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ {"match": { │ │
│ │ "title": { │ │
│ │ "query": "小米 13 Pro 手机", │ │
│ │ "minimum_should_match": "75%" // 至少匹配75% │ │
│ │ } │ │
│ │ }} │ │
│ │ 分词: [小米, 13, Pro, 手机] → 4个词 │ │
│ │ 至少匹配 ceil(4*75%) = 3个词 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.2 multi_match —— 跨字段搜索
┌─────────────────────────────────────────────────────────────┐
│ multi_match 跨字段搜索 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 场景:用户在搜索框输入"小米",你希望同时在 title、 │
│ description、brand 三个字段中搜索 │
│ │
│ 六种匹配模式: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ best_fields (默认):取单个最佳匹配字段的分数 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ title: "小米" → score: 8.5 ← 取这个 │ │
│ │ │ description: "小米" → score: 2.1 │ │
│ │ │ brand: "小米" → score: 5.0 │ │
│ │ │ 最终分数: max(8.5, 2.1, 5.0) = 8.5 │ │
│ │ │ │ │
│ │ │ 适用:搜索词在同一字段里匹配最好 │ │
│ │ │ 如搜索"小米手机",title 完整匹配 > │ │
│ │ │ description 部分匹配 │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ most_fields:取所有匹配字段的分数之和 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ title: "小米" → score: 5.0 │ │
│ │ │ description: "小米" → score: 2.0 │ │
│ │ │ 最终分数: 5.0 + 2.0 = 7.0 │ │
│ │ │ │ │
│ │ │ 适用:同一文本出现在多个字段中应加分 │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ cross_fields:把多个字段当一个大字段处理 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ "张三 北京" │ │
│ │ │ first_name: "张三" address: "北京" │ │
│ │ │ 两个词分散在不同字段,但应该匹配! │ │
│ │ │ │ │
│ │ │ 适用:搜索词分散在多个字段中的场景 │ │
│ │ │ 如人名+地名、产品名+型号 │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ phrase / phrase_prefix / bool_prefix │ │
│ │ 以 phrase 方式跨字段匹配 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.3 match_phrase —— 短语匹配
┌─────────────────────────────────────────────────────────────┐
│ match_phrase 短语查询 │
├─────────────────────────────────────────────────────────────┤
│ │
│ match_phrase 要求词按顺序连续出现(利用 Position 数据) │
│ │
│ 示例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ {"match_phrase": {"title": "小米手机"}} │ │
│ │ │ │
│ │ 分词: [小米, 手机] │ │
│ │ │ │
│ │ 文档1: "小米 13 手机" │ │
│ │ 小米(position=0), 手机(position=2) │ │
│ │ → 位置差 = 2,不连续 ❌ 不匹配! │ │
│ │ │ │
│ │ 文档2: "小米手机壳" │ │
│ │ 小米(position=0), 手机(position=1) │ │
│ │ → 位置差 = 1,连续 ✅ 匹配! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ slop 参数:允许词之间有间隔 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ {"match_phrase": { │ │
│ │ "title": { │ │
│ │ "query": "小米手机", │ │
│ │ "slop": 2 // 允许词之间最多间隔2个位置 │ │
│ │ } │ │
│ │ }} │ │
│ │ │ │
│ │ "小米 13 手机" → slop=1, slop=2 ✅ 匹配 │ │
│ │ "小米 13 Pro 手机" → slop=2, slop=2 ✅ 匹配 │ │
│ │ "小米 13 Pro Max 手机" → slop=3 ❌ 不匹配 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.4 query_string —— 类 Lucene 语法
┌─────────────────────────────────────────────────────────────┐
│ query_string 高级查询 │
├─────────────────────────────────────────────────────────────┤
│ │
│ query_string 支持类 Lucene 的查询语法,适合高级用户 │
│ │
│ 语法速查: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "小米 AND 手机" → 两个词都必须有 │ │
│ │ "小米 OR 华为" → 任意一个有 │ │
│ │ "小米 NOT 二手" → 有小米但没有二手 │ │
│ │ "title:小米" → 只在 title 字段搜 │ │
│ │ "price:[100 TO 500]" → 价格范围 │ │
│ │ "name:/mi.*/" → 正则匹配 │ │
│ │ "小米~" → 模糊匹配(编辑距离1) │ │
│ │ "\"小米手机\"~2" → 短语匹配,slop=2 │ │
│ │ "小米^2 手机" → boosting,小米权重×2 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 危险与建议: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ⚠️ query_string 容易造成查询错误 │ │
│ │ 用户输入 "NOT" 或特殊字符可能导致意外行为 │ │
│ │ │ │
│ │ ✅ 用户搜索 → match / multi_match │ │
│ │ ✅ 后台管理 → query_string(可控的专家查询) │ │
│ │ ✅ 更安全:simple_query_string(忽略错误语法) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第3部分:复合查询
3.1 bool 查询 —— 一切皆 bool
┌─────────────────────────────────────────────────────────────┐
│ bool 查询四子句 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 在 ES 中,几乎所有复杂查询最终都是 bool 查询的组合: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "query": { │ │
│ │ "bool": { │ │
│ │ "must": [...], // 必须匹配(AND) │ │
│ │ "should": [...], // 应该匹配(OR) │ │
│ │ "filter": [...], // 必须匹配(不评分) │ │
│ │ "must_not": [...] // 必须不匹配(NOT) │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 四个子句的评分行为: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 子句 匹配要求 影响评分 缓存行为 │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ must 必须匹配 影响_score 不缓存 │ │
│ │ filter 必须匹配 不影响_score 缓存! │ │
│ │ should 可选匹配 匹配的越多 不缓存 │ │
│ │ 分数越高 │ │
│ │ must_not 必须不匹配 不影响_score 缓存! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键规则:如果 bool 中没有 must/filter, │
│ should 中至少一个必须匹配 │
│ │
└─────────────────────────────────────────────────────────────┘3.2 bool 查询实战:电商搜索
┌─────────────────────────────────────────────────────────────┐
│ 电商搜索完整 bool 查询 │
├─────────────────────────────────────────────────────────────┤
│ │
│ { │
│ "query": { │
│ "bool": { │
│ "must": [ // 必须匹配 │
│ { │
│ "multi_match": { │
│ "query": "小米手机", │
│ "fields": ["title^3", "description^1.5", │
│ "brand^2"], │
│ "type": "best_fields" │
│ } │
│ } │
│ ], │
│ "filter": [ // 硬过滤(不评分) │
│ {"term": {"status": "active"}}, │
│ {"range": {"price": {"gte": 1000, "lte": 5000}}}, │
│ {"term": {"category": "phone"}} │
│ ], │
│ "should": [ // 加分项 │
│ {"term": {"has_discount": true}}, │
│ {"term": {"free_shipping": true}}, │
│ {"range": {"rating": {"gte": 4.5}}} │
│ ], │
│ "must_not": [ // 排除项 │
│ {"term": {"condition": "used"}} │
│ ] │
│ } │
│ }, │
│ "sort": [ // 排序 │
│ {"_score": "desc"}, │
│ {"sales_count": "desc"} │
│ ] │
│ } │
│ │
└─────────────────────────────────────────────────────────────┘3.3 dis_max 与 function_score
┌─────────────────────────────────────────────────────────────┐
│ dis_max 与 function_score │
├─────────────────────────────────────────────────────────────┤
│ │
│ dis_max:取最佳匹配子句的分数(而非求和) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "dis_max": { │ │
│ │ "queries": [ │ │
│ │ {"match": {"title": "小米手机"}}, │ │
│ │ {"match": {"description": "小米手机"}} │ │
│ │ ], │ │
│ │ "tie_breaker": 0.3 // 次佳分数 × 0.3 加进来 │ │
│ │ } │ │
│ │ } │ │
│ │ final_score = max_score + tie_breaker × other_scores │ │
│ │ │ │
│ │ 适用:同一个内容出现在多个字段时,不希望重复加分 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ function_score:自定义评分公式 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "function_score": { │ │
│ │ "query": {"match": {"title": "小米手机"}}, │ │
│ │ "functions": [ │ │
│ │ { │ │
│ │ "field_value_factor": { // 字段值因子 │ │
│ │ "field": "sales_count", │ │
│ │ "factor": 1.2, // 销量×1.2 │ │
│ │ "modifier": "log1p" // 用 log 平滑 │ │
│ │ } │ │
│ │ }, │ │
│ │ { │ │
│ │ "gauss": { // 衰减函数 │ │
│ │ "created_at": { │ │
│ │ "origin": "now", // 新商品加分 │ │
│ │ "scale": "30d", // 30天后衰减 │ │
│ │ "decay": 0.5 // 衰减50% │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ │ ], │ │
│ │ "score_mode": "multiply" // 函数之间相乘 │ │
│ │ } │ │
│ │ } │ │
│ │ 最终: BM25得分 × 销量因子 × 新鲜度因子 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第4部分:聚合(Aggregations)
4.1 聚合三大类型
┌─────────────────────────────────────────────────────────────┐
│ 聚合三大类型全景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Bucket Aggregation(分桶聚合) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 把文档分成多个"桶" │ │
│ │ │ │ │
│ │ │ terms: "category" → {phone: 150, laptop: 80} │ │
│ │ │ range: "price" → {0-1000: 30, ...} │ │
│ │ │ date_histogram: "created_at" → 按月分桶 │ │
│ │ │ filter: 只聚合满足条件的文档 │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Metrics Aggregation(指标聚合) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 对桶内文档计算统计指标 │ │
│ │ │ │ │
│ │ │ avg: 平均价格 → 3267.50 │ │
│ │ │ sum: 总销量 → 12345 │ │
│ │ │ min/max: 最低/最高价格 │ │
│ │ │ stats: 一次性返回 count/min/max/avg/sum │ │
│ │ │ cardinality: 近似去重计数(UV 等场景) │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Pipeline Aggregation(管道聚合) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 对聚合结果再做聚合 │ │
│ │ │ │ │
│ │ │ derivative: 环比增长(本月 vs 上月) │ │
│ │ │ moving_avg: 移动平均(平滑趋势线) │ │
│ │ │ cumulative_sum: 累计求和(累计到当前的销量) │ │
│ │ │ bucket_script: 自定义计算(利润率 = 利润/收入)│ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.2 聚合实战:商品搜索面板
┌─────────────────────────────────────────────────────────────┐
│ 商品搜索 + Facet 聚合 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 搜索"手机"时,同时返回: │
│ • 品牌分布(facet) │
│ • 价格区间 │
│ • 各分类下的平均价格 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GET /products/_search │ │
│ │ { │ │
│ │ "query": {"match": {"title": "手机"}}, │ │
│ │ "size": 0, // 不返回文档 │ │
│ │ "aggs": { │ │
│ │ "brands": { // 品牌分布 │ │
│ │ "terms": {"field": "brand.keyword", │ │
│ │ "size": 10} │ │
│ │ }, │ │
│ │ "price_ranges": { // 价格区间 │ │
│ │ "range": { │ │
│ │ "field": "price", │ │
│ │ "ranges": [ │ │
│ │ {"to": 1000}, │ │
│ │ {"from": 1000, "to": 3000}, │ │
│ │ {"from": 3000, "to": 5000}, │ │
│ │ {"from": 5000} │ │
│ │ ] │ │
│ │ } │ │
│ │ }, │ │
│ │ "avg_price_by_brand": { // 嵌套聚合 │ │
│ │ "terms": {"field": "brand.keyword"}, │ │
│ │ "aggs": { │ │
│ │ "avg_price": {"avg": {"field": "price"}} │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.3 terms 聚合的准确性问题
┌─────────────────────────────────────────────────────────────┐
│ terms 聚合的近似准确性问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ terms 聚合默认是近似的(基于每个 shard 的 top N)—— │
│ 分片数越多,准确度越低 │
│ │
│ 场景:3 个 shard,每个 shard 返回 top 3 品牌 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Shard 0: [华为:50, 苹果:45, 小米:40] │ │
│ │ Shard 1: [华为:48, 三星:30, 小米:28] │ │
│ │ Shard 2: [苹果:55, OPPO:20, 小米:18] │ │
│ │ │ │
│ │ 汇总(coordinating node 只看到 top 3×3=9条): │ │
│ │ 华为: 98, 苹果: 100, ... │ │
│ │ │ │
│ │ 问题:vivo 在 Shard 1 有 29 个文档(排第4), │ │
│ │ 但 Shard 2 只有 5 个(排第20) │ │
│ │ vivo 在 Shard 1 被截断,汇总时丢失! │ │
│ │ 实际总量 vivo: 34 > 三星: 30 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 解决方式: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. shard_size 设大(默认 size×1.5+10,可手动调大) │ │
│ │ 2. 如果 accuracy 至关重要,数据量不大时: │ │
│ │ {"terms": {"field": "brand", │ │
│ │ "shard_size": 10000}} │ │
│ │ 3. 设置 index 只有 1 个主分片(牺牲扩展性换准确度) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第5部分:相关性调优
5.1 BM25 参数调优
┌─────────────────────────────────────────────────────────────┐
│ BM25 参数调优 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 评分公式: │
│ Score(D,Q) = Σ IDF(qi) × (f(qi,D) × (k1 + 1)) │
│ / (f(qi,D) + k1 × (1 - b + │
│ b × |D| / avgdl)) │
│ │
│ 两个关键参数: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ k1 (默认 1.2):词频饱和度控制 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ k1 越大 → 词频的影响越大 │ │ │
│ │ │ k1 越小 → 词频更快饱和 │ │ │
│ │ │ │ │ │
│ │ │ 例子: │ │ │
│ │ │ 文档A: "手机" 出现 1 次 │ │ │
│ │ │ 文档B: "手机" 出现 10 次 │ │ │
│ │ │ │ │ │
│ │ │ k1=1.2: B 得分 ≈ A×3 │ │ │
│ │ │ k1=0.5: B 得分 ≈ A×1.8 (差距缩小) │ │ │
│ │ │ k1=3.0: B 得分 ≈ A×5 (差距拉大) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ b (默认 0.75):文档长度归一化程度 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ b=0 → 不考虑文档长度(长文档不受惩罚) │ │ │
│ │ │ b=1 → 完全按文档长度归一化(长文档严重惩罚)│ │ │
│ │ │ b=0.75 → 折中(默认值) │ │ │
│ │ │ │ │ │
│ │ │ 适用: │ │ │
│ │ │ 短文档(tweet、标题)→ b 可以小一些 │ │ │
│ │ │ 长文档(文章、书籍)→ b 接近默认值 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘5.2 字段权重与 boosting
┌─────────────────────────────────────────────────────────────┐
│ 权重设计策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 权重决定"哪个字段的匹配更重要": │
│ │
│ 索引时权重(不推荐): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "title": {"type": "text", "boost": 3} │ │
│ │ 问题:权重写死在 mapping 里,改权重需要 reindex │ │
│ │ ES 7.x+ 已废弃索引时 boost │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 查询时权重(推荐): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "fields": ["title^3", "brand^2", "description^1"] │ │
│ │ ^ 后接的数值表示权重倍数 │ │
│ │ │ │
│ │ 经验值: │ │
│ │ • title: 3-5(标题是最强信号) │ │
│ │ • brand/tags: 2-3(品牌标签次之) │ │
│ │ • description: 1-2(内容权重最低) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 权重设计原则: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 字段越短,权重应该越高 │ │
│ │ → 短字段匹配 = 高信号,长字段匹配 = 噪声 │ │
│ │ 2. 字段离用户意图越近,权重应该越高 │ │
│ │ → 标题 > 摘要 > 正文 │ │
│ │ 3. A/B 测试决定最终权重——没有万能公式 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第6部分:常见查询模式
6.1 自动补全(Autocomplete)
┌─────────────────────────────────────────────────────────────┐
│ 自动补全三种方案 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方案1:Completion Suggester(最快) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Mapping: │ │
│ │ "suggest": {"type": "completion"} │ │
│ │ │ │
│ │ 查询: │ │
│ │ {"suggest": {"title-suggest": { │ │
│ │ "prefix": "小米", │ │
│ │ "completion": {"field": "suggest"} │ │
│ │ }}} │ │
│ │ │ │
│ │ 特点:基于 FST,毫秒级响应 │ │
│ │ 限制:只能前缀匹配,不支持中间匹配 │ │
│ │ 适用:搜索框实时补全 (Search-as-you-type) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 方案2:Edge N-Gram Tokenizer(灵活) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "小米手机" → 小, 小米, 小米手, 小米手机 │ │
│ │ 搜索"小米" → 匹配"小米" token → 命中 │ │
│ │ │ │
│ │ 特点:用标准 match 查询,灵活性高 │ │
│ │ 限制:索引体积膨胀明显 │ │
│ │ 适用:对补全质量要求高、需要过滤/排序的场景 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 方案3:Search-as-you-type(中间匹配) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Mapping: "title": {"type": "search_as_you_type"} │ │
│ │ 查询: {"multi_match": {"query": "小米", │ │
│ │ "type": "bool_prefix"}} │ │
│ │ │ │
│ │ 支持:前缀+中间匹配+多字段 │ │
│ │ 适用:ES 7.x+,兼顾速度和灵活性 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘6.2 模糊搜索与纠错
┌─────────────────────────────────────────────────────────────┐
│ 模糊搜索实现方案 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 场景:用户输入 "ipone",想找 "iPhone" │
│ │
│ 方案1:Fuzzy Query(编辑距离) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ {"fuzzy": {"title": {"value": "ipone", │ │
│ │ "fuzziness": "AUTO"}}} │ │
│ │ │ │
│ │ fuzziness 规则: │ │
│ │ • 0-2 字符:必须完全匹配 │ │
│ │ • 3-5 字符:允许 1 个编辑距离 │ │
│ │ • >5 字符:允许 2 个编辑距离 │ │
│ │ │ │
│ │ 编辑距离 = 增/删/改一个字符的次数 │ │
│ │ "ipone" → "iphone": 1次操作(插入h) → 可匹配 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 方案2:同义词 + 拼音搜索(中文场景) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 同义词过滤器: │ │
│ │ "小米, xiaomi, 粗粮手机" → 全映射为"小米" │ │
│ │ │ │
│ │ 拼音分析器: │ │
│ │ "小米" → [xiao, mi] │ │
│ │ 用户输入拼音 "xiaomi" → 也能搜到"小米" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 方案3:混合策略(推荐) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 先用精确 match 查询(快速) │ │
│ │ 2. 如果结果太少(< 5条),自动 fallback: │ │
│ │ a. fuzzy query(拼写纠错) │ │
│ │ b. 同义词扩展 │ │
│ │ c. 拼音匹配 │ │
│ │ 3. 前端展示"您是不是要找:iPhone?" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第7部分:查询性能优化
7.1 Profile API —— 查询性能诊断
┌─────────────────────────────────────────────────────────────┐
│ Profile API 使用 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 在查询中加 "profile": true 可以获得详细的性能分解: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "profile": true, │ │
│ │ "query": { ... } │ │
│ │ } │ │
│ │ │ │
│ │ 输出: │ │
│ │ { │ │
│ │ "profile": { │ │
│ │ "shards": [{ │ │
│ │ "id": "[xxx][0]", │ │
│ │ "searches": [{ │ │
│ │ "query": [{ │ │
│ │ "type": "BooleanQuery", │ │
│ │ "description": "title:小米 title:手机", │ │
│ │ "time_in_nanos": 1523400, │ │
│ │ "breakdown": { │ │
│ │ "create_weight": 23400, │ │
│ │ "build_scorer": 890000, ← 慢在这里! │ │
│ │ "next_doc": 580000, ← 也慢 │ │
│ │ "advance": 0, │ │
│ │ "score": 30000 │ │
│ │ } │ │
│ │ }] │ │
│ │ }] │ │
│ │ }] │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ breakdown 字段含义: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ create_weight → 构建查询权重的时间 │ │
│ │ build_scorer → 构建评分器(读取倒排表最耗时) │ │
│ │ next_doc → 遍历匹配文档的时间 │ │
│ │ advance → 跳表跳跃的时间 │ │
│ │ score → 计算评分的时间 │ │
│ │ │ │
│ │ build_scorer 慢 → 倒排表太大,考虑分词优化 │ │
│ │ next_doc 慢 → 匹配文档太多,加 filter 缩小范围 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘7.2 Filter 缓存与 Routing
┌─────────────────────────────────────────────────────────────┐
│ 查询性能优化三板斧 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 优化1:善用 Filter Context(最重要) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 把不需要评分的条件放进 filter,而不是 must │ │
│ │ filter 结果自动缓存(LRU,默认缓存 10% heap) │ │
│ │ │ │
│ │ 缓存效果验证: │ │
│ │ GET /_stats/query_cache?human │ │
│ │ → cache_size, evictions, hit_count, miss_count │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 优化2:Routing —— 精准定位 shard │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 默认:查询广播到所有 shard,每个 shard 都要搜 │ │
│ │ 使用 routing:根据某个字段值路由到特定 shard │ │
│ │ │ │
│ │ 场景:按 userId 搜索订单 │ │
│ │ PUT /orders/_doc/123?routing=user_456 │ │
│ │ GET /orders/_search?routing=user_456 │ │
│ │ │ │
│ │ 代价:单 shard 查询,延迟从 50ms → 5ms │ │
│ │ 限制:查询必须包含 routing 值,否则搜不到 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 优化3:限制搜索范围 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 使用 index pattern 限制搜索的索引 │ │
│ │ • 用 filter 先缩小文档范围(时间范围等) │ │
│ │ • 设置 terminate_after 限制扫描文档数 │ │
│ │ • 设置 timeout 防止慢查询拖垮集群 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘7.3 慢查询日志与分析
┌─────────────────────────────────────────────────────────────┐
│ 慢查询日志配置 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 配置慢查询阈值: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PUT /_cluster/settings │ │
│ │ { │ │
│ │ "transient": { │ │
│ │ "index.search.slowlog.threshold.query.warn": │ │
│ │ "500ms", │ │
│ │ "index.search.slowlog.threshold.query.info": │ │
│ │ "200ms", │ │
│ │ "index.search.slowlog.level": "info" │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 慢查询日志输出示例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [2024-01-15T10:23:45,123][INFO ][index.search.slowlog│ │
│ │ query] [node-1] [products/0] │ │
│ │ took[850.5ms], took_millis[850], │ │
│ │ total_hits[12345], │ │
│ │ source[{"query":{"match":{"title":"手机"}}}] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 慢查询常见原因与对策: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 原因 对策 │ │
│ │ ─────────────────────────────────────────────── │ │
│ │ match 高基数字段 加 filter 缩小范围 │ │
│ │ wildcard 前导通配 改用 ngram 或 edge ngram │ │
│ │ 深度分页 (from > 1万) 改用 search_after │ │
│ │ 大聚合 (cardinality) 用预计算或 approximate │ │
│ │ 脚本评分 改用索引时预计算字段 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘核心总结
总结1:Query vs Filter
Query Context (must/should) → 评分、不缓存、用于全文匹配
Filter Context (filter) → 不评分、自动缓存、用于精确筛选
原则:全文匹配用 Query,精确筛选用 Filter总结2:bool 查询四子句
| 子句 | 必须匹配 | 评分 | 缓存 |
|---|---|---|---|
| must | 是 | 影响 | 否 |
| filter | 是 | 不影响 | 是 |
| should | 否(无must时是) | 匹配越多分越高 | 否 |
| must_not | 必须不匹配 | 不影响 | 是 |
总结3:相关性调优层次
Layer 0: 正确使用 Query vs Filter(基础)
Layer 1: multi_match + 字段权重(^3, ^2, ^1)
Layer 2: function_score(销量、新鲜度等因素)
Layer 3: 自定义 BM25 参数(k1, b)
Layer 4: script_score(自定义评分逻辑)总结4:聚合三大类型
Bucket → 分桶(terms, range, date_histogram)
Metrics → 桶内统计(avg, sum, min/max, stats, cardinality)
Pipeline → 聚合的聚合(derivative, moving_avg, cumulative_sum)章节测试
测试1:Query vs Filter
以下查询中,哪个应该放在 filter 里,哪个应该放在 must 里? A. 标题包含"小米" B. 价格在 1000-5000 C. 商品状态为"上架" D. 描述包含"性价比"
测试2:multi_match
best_fields、most_fields、cross_fields 三种模式分别适用于什么场景?
测试3:bool 查询
在没有 must 子句的情况下,should 子句的行为是什么?
测试4:聚合
terms 聚合为什么默认不准确?如何提高准确度?
测试5:相关性
文档 A 标题"小米手机"、描述"一部好用的智能手机"; 文档 B 标题"智能手机推荐"、描述"小米、华为、苹果等品牌手机对比"。 搜索"小米手机"时,BM25 会给哪个文档更高分?为什么?
测试6:性能
用户抱怨搜索越来越慢,你应该按什么顺序排查?
测试7:综合设计
设计一个电商搜索接口,支持:关键词搜索、品牌筛选、价格区间、按销量+相关性排序、搜索词自动补全、拼写纠错。写出核心的 ES DSL 结构和 mapping 设计要点。
参考答案
测试1答案
答案:
- A(标题包含"小米")→ must(全文匹配,需要评分)
- B(价格范围)→ filter(精确筛选,不需要评分,可缓存)
- C(商品状态)→ filter(精确筛选,可缓存)
- D(描述包含"性价比")→ should(可选加分项,匹配更好但非必须)
测试2答案
答案:
best_fields:搜索词集中在单个字段最佳时(如"小米13Pro" → title 完整匹配)most_fields:同一内容出现在多个字段时(如 title 和 description 都包含"小米")cross_fields:搜索词分散在不同字段时(如"张三 北京" → name 和 address 各含一词)
测试3答案
答案:如果 bool 查询中既没有 must 也没有 filter,则 should 中至少一条必须匹配(变为 required)。即 "(must 为空) → should 变为必须至少匹配一个"。
如果 bool 中已有 must 或 filter,则 should 中的条件匹配越多得分越高,但不强制匹配。
测试4答案
答案:terms 聚合在每个 shard 上独立计算 top N,然后由协调节点汇总。如果某个 term 在某些 shard 排名靠后(被截断),汇总时会丢失数据。提高准确度:增大 shard_size 参数、减少分片数(如 1 个主分片)、或使用 composite aggregation 分页遍历所有桶。
测试5答案
答案:文档 A 分数更高。理由:
- 文档 A 标题"小米手机"是完整的精确匹配(两个词连续出现在短字段中)
- 文档 A 描述也包含"手机",形成跨字段加分
- BM25 的 IDF 会给"小米"这个词较高的权重(相比"手机"更稀有)
- 文档长度归一化(b=0.75)使得短文档占优
- 文档 B 虽然标题包含"手机"、描述包含"小米",但它们分散在不同字段和较长的文本中
测试6答案
答案:
- 慢查询日志 → 找到最慢的查询
- Profile API → 定位慢在哪个阶段(build_scorer / next_doc / score)
- 检查是否滥用 wildcard / leading wildcard
- 检查 query context vs filter context 是否正确使用
- 检查深度分页(from 是否过大)
- 检查聚合是否使用了高基数 cardinality
- 检查 filter cache 命中率
- 检查 shard 数量和集群健康状态
测试7答案
答案:
核心 mapping 要点:
{
"title": {
"type": "text", "analyzer": "ik_max_word",
"fields": {
"keyword": {"type": "keyword"},
"completion": {"type": "completion"},
"edge_ngram": {"type": "text", "analyzer": "edge_ngram_analyzer"}
}
},
"brand": {"type": "keyword"},
"price": {"type": "float"},
"sales_count": {"type": "integer"},
"status": {"type": "keyword"}
}核心查询 DSL:
{
"query": {
"bool": {
"must": [{"multi_match": {"query": "小米", "fields": ["title^3","description^1.5"]}}],
"filter": [
{"terms": {"brand.keyword": ["小米","华为"]}},
{"range": {"price": {"gte":1000,"lte":5000}}},
{"term": {"status": "active"}}
]
}
},
"sort": [{"_score":"desc"},{"sales_count":"desc"}]
}补全和纠错用独立的 suggest API 和 fuzzy 查询,作为独立的接口路径。
相关笔记
- [[./00-overview]] - 搜索引擎概览
- [[./03-elasticsearch-cluster]] - ES 集群规划与运维
- [[/03-web/07-middleware/06-elasticsearch]] - ES 倒排索引深入
下一步学习
- [ ] 阅读 03 - Elasticsearch 集群规划与运维
- [ ] 阅读 04 - Meilisearch 与替代方案
学习状态:🟡 开始学习