精通Elasticsearch查询DSL:数据检索必备命令

通过掌握查询DSL,释放Elasticsearch检索的强大功能。本指南深入解析关键的JSON查询结构,重点介绍`match`、`term`和范围查询的实际应用。了解基础`bool`查询中`must`(评分)和`filter`(缓存)子句的关键区别,助您高效构建复杂且高性能的数据搜索。

精通Elasticsearch查询DSL:数据检索必备命令

当简单的搜索框无法满足需求时,Elasticsearch查询DSL就是您需要的JSON语言。它允许您在一次请求中混合全文搜索、精确过滤、日期范围、排序、分页和聚合。这种灵活性很有用,但也容易编写出返回错误文档的查询,或者在测试中运行良好但在生产环境中变慢的查询。

学习查询DSL的最佳方法是牢记两个问题:“我是在搜索文本以获取相关性吗?”以及“我是在过滤精确值吗?”大多数查询选择都源于这种区分。

Elasticsearch搜索请求的结构

所有Elasticsearch搜索都是针对特定索引(或多个索引)的_search端点执行的。一个基本的搜索请求是一个包含定义查询参数的JSON主体的POST请求。该主体最关键的部分是query对象。

基本结构:

POST /your_index_name/_search
{
  "query": { ... 在此处定义查询结构 ... },
  "size": 10, 
  "from": 0
}

核心查询类型:精确性与相关性

查询DSL提供了多种针对不同数据类型和匹配需求定制的查询。查询的选择会显著影响相关性评分和性能。

1. 全文搜索:match查询

match查询是在已分析字段上进行全文搜索的标准。它会将搜索词进行分词,并在指定字段中检查匹配的词元。

使用场景: 搜索自然语言文本,且相关性评分很重要。

示例: 查找'description'字段包含'cloud'或'computing'的文档。

GET /products/_search
{
  "query": {
    "match": {
      "description": "cloud computing"
    }
  }
}

2. 精确值匹配:term查询

term查询搜索包含指定精确词元的文档。与match不同,它不会对搜索字符串进行分析,因此非常适合对关键词、ID或数值索引字段进行精确匹配。

使用场景: 在非分析字段(如keyword字段或数字)上按精确值过滤。

示例: 检索具有精确ID SKU10021的产品。

GET /products/_search
{
  "query": {
    "term": {
      "product_id": "SKU10021"
    }
  }
}

3. 范围查询

范围查询允许您过滤字段值在指定范围内的文档(数值、日期或字符串)。

语法: 使用gt(大于)、gte(大于等于)、lt(小于)和lte(小于等于)。

示例: 查找2024年1月1日之后下的订单。

GET /orders/_search
{
  "query": {
    "range": {
      "order_date": {
        "gte": "2024-01-01",
        "lt": "2025-01-01"
      }
    }
  }
}

4. 按存在性过滤:exists查询

exists查询用于识别指定字段存在(即非空或未缺失)的文档。

示例: 查找所有提供了电子邮件地址的用户。

GET /users/_search
{
  "query": {
    "exists": {
      "field": "email_address"
    }
  }
}

使用bool查询构建复杂逻辑

对于几乎所有实际的搜索应用,您都需要组合多个条件。bool查询是实现此目的的基本工具,它允许您使用布尔逻辑组合其他查询子句。

bool中的子句

bool查询接受四个主要子句:

  1. must:此数组中的所有子句必须匹配。must中的子句会影响相关性评分。
  2. filter:此数组中的所有子句必须匹配,但它们在不评分的上下文中执行。这使得它们对于严格的包含/排除条件更快。
  3. should:此数组中至少有一个子句应该匹配。这些子句会影响相关性评分,但对于匹配是可选的。
  4. must_not:此数组中的所有子句都不能匹配(相当于逻辑非)。

实际的bool查询示例

让我们结合几个概念来查找提及'security'但排除草稿且在美国地区可用的高优先级文档。

GET /logs/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content": "security breach"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "region.keyword": "US"
          }
        }
      ],
      "should": [
        {
          "term": {
            "priority": 5
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "status.keyword": "DRAFT"
          }
        }
      ]
    }
  }
}

示例说明:

  • Must: 文档必须在已分析的内容字段中包含短语“security breach”。
  • Filter: 文档必须标记为“US”地区(快速、精确匹配)。
  • Should: 匹配priority: 5的文档将在其相关性评分中获得提升,但满足mustfilter子句但优先级较低的文档仍会被返回。
  • Must Not: 标记为“DRAFT”的文档被严格排除。

查询构建的最佳实践

为确保搜索既准确又高效,请遵循以下准则:

  • 对于非评分条件,优先使用filter而非must 如果您只检查包含/排除(例如,按ID、精确日期或状态过滤),请始终在bool查询中使用filter子句。这可以利用缓存并避免昂贵的评分计算。
  • 明智地使用精确查询: 对于映射为text(已分析)的字段,使用match。对于映射为keyword(未分析)的字段,使用term或范围查询。
  • 避免深层嵌套: 虽然可能,但深层嵌套的bool查询可能难以阅读和调试,有时还可能导致性能下降。
  • 利用minimum_should_match 对于should子句,设置minimum_should_match(例如,设置为12)会强制满足一定数量的可选条件,从而有效地将它们转变为必需条件,同时仍允许它们对评分做出贡献。

映射决定查询的合理性

大多数查询DSL错误都源于映射。如果字段的映射与您认为的不同,即使查询看起来正确,也可能返回令人困惑的结果。

一种常见模式是带有keyword子字段的text字段:

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "status": { "type": "keyword" },
      "created_at": { "type": "date" },
      "price": { "type": "double" }
    }
  }
}

当您需要已分析的全文行为时,在title上使用match。当您需要精确的标题值时,在title.keyword上使用term。在status上使用term,因为它已经是keyword。在created_atprice上使用range,因为这些字段是日期和数值。

如果对text字段的term查询没有按预期工作,问题通常在于分析。存储的词元可能已被转换为小写、拆分、词干提取或以其他方式更改。在更改查询之前检查映射。

GET /products/_mapping

对于文本分析问题,_analyze很有用:

GET /products/_analyze
{
  "field": "description",
  "text": "Cloud Computing"
}

这将显示Elasticsearch将搜索哪些词元。

matchmatch_phrasemulti_match

match是日常使用的全文查询,但它不是您会使用的唯一查询。

当单词顺序重要时,使用match_phrase

GET /products/_search
{
  "query": {
    "match_phrase": {
      "description": "wireless charging stand"
    }
  }
}

这对于产品名称、日志消息、文档标题以及精确序列具有意义的短语很有用。它比match更严格,因此可能返回更少的文档。

当相同的用户输入应搜索多个字段时,使用multi_match

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "noise cancelling headphones",
      "fields": ["title^3", "description", "brand^2"]
    }
  }
}

^3^2的提升告诉Elasticsearch,在titlebrand中的匹配应比在description中的匹配权重更高。提升并不能保证文档会排名第一;它只是一个评分提示。在过于激进地调整提升之前,请使用真实查询进行测试。

不损害集群的分页

基本的fromsize参数适用于浅分页:

GET /products/_search
{
  "from": 20,
  "size": 10,
  "query": {
    "match": {
      "description": "laptop sleeve"
    }
  }
}

深分页则不同。请求第1000页会强制Elasticsearch排序并跳过许多结果。对于面向用户的搜索,避免无限制的深分页。对于导出或后台扫描,使用带有稳定排序的search_after

GET /products/_search
{
  "size": 100,
  "sort": [
    { "created_at": "asc" },
    { "_id": "asc" }
  ],
  "search_after": ["2025-01-10T12:00:00Z", "abc123"],
  "query": {
    "term": {
      "status": "active"
    }
  }
}

search_after中的值来自上一个响应中最后一个命中的sort数组。这种方法对于遍历大型结果集更稳定。

源过滤保持响应有用

搜索性能不仅仅是查询执行。返回巨大的文档可能会减慢客户端、网络和协调节点的速度。如果UI只需要几个字段,请请求这些字段:

GET /orders/_search
{
  "_source": ["order_id", "customer_id", "total", "created_at", "status"],
  "query": {
    "bool": {
      "filter": [
        { "term": { "status": "paid" } },
        { "range": { "created_at": { "gte": "now-7d/d" } } }
      ]
    }
  }
}

这使响应更易于阅读,并可以减少有效负载大小。它不能替代良好的索引设计,但当文档包含当前页面不需要的大型描述、元数据blob或嵌套数组时,它会有所帮助。

排序和聚合需要正确的字段

对已分析的文本进行排序通常是个错误。对keyword、数值或日期字段进行排序:

GET /products/_search
{
  "sort": [
    { "price": "asc" },
    { "title.keyword": "asc" }
  ],
  "query": {
    "term": {
      "status": "active"
    }
  }
}

同样的原则适用于许多聚合。如果您想要按状态的计数,请在keyword字段上进行聚合:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "orders_by_status": {
      "terms": {
        "field": "status"
      }
    }
  },
  "query": {
    "range": {
      "created_at": {
        "gte": "now-30d/d"
      }
    }
  }
}

size: 0告诉Elasticsearch您只想要聚合结果,而不是匹配的文档。这是一个保持响应更简洁的小习惯。

使用explainprofile调试查询

当结果排名异常时,对单个文档使用explain

GET /products/_explain/SKU10021
{
  "query": {
    "match": {
      "description": "cloud computing"
    }
  }
}

当查询缓慢时,在非生产环境或仔细控制的生产测试中使用profile

GET /products/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        { "match": { "description": "cloud computing" } }
      ],
      "filter": [
        { "term": { "status": "active" } }
      ]
    }
  }
}

profile输出很冗长,但它可以显示时间花费在文本查询、过滤器、脚本还是请求的其他部分。不要在应用程序代码中启用profiling;将其用作调试工具。

合理的查询构建习惯

对于大多数应用程序搜索,按以下顺序构建请求:

  1. 将精确约束放入filter:租户ID、状态、地区、日期窗口、权限。
  2. 将用户输入的文本放入must,使用matchmatch_phrasemulti_match
  3. 使用should进行排名偏好设置,而不是硬性要求,除非您设置了minimum_should_match
  4. _source限制为调用者需要的字段。
  5. 如果分页或导出很重要,添加稳定的排序。
  6. 在责怪Elasticsearch之前检查映射。

查询DSL之所以强大,是因为它分离了过滤、评分、排序和响应塑造。一旦您将这些任务分开,查询就变得更容易阅读、更容易调整,并且在生产环境中更少出现意外。

一个小型故障排除示例

假设用户搜索ACME-1000但没有得到结果,即使产品存在。不要立即添加通配符。首先检查映射。如果skukeyword,这应该有效:

GET /products/_search
{
  "query": {
    "term": {
      "sku": "ACME-1000"
    }
  }
}

如果sku意外映射为text,分析可能已拆分或更改了该值。在某些情况下您仍然可以查询它,但更好的修复方法通常是对未来索引进行映射更改。精确标识符、状态、地区和租户ID应该是keyword-like字段。人工编写的描述和标题应该是text字段。当映射与人们实际检索数据的方式匹配时,查询DSL会变得容易得多。