结构化搜索

指查询有关内在结构的过程。

  • 精确值查找
  • 组合过滤器
  • 查找多个精确值
  • 范围
  • 处理 Null 值
  • 关于缓存

    精确值查询

tg:
sql:

  1. SELECT document
  2. FROM products
  3. WHERE price = 20

es:

通常当查找一个精确值的时候,我们不希望对查询进行评分计算。只希望对文档进行包括或排除的计算,constant_score 查询,它包含一个 term 查询

  1. GET /my_store/products/_search
  2. {
  3. "query" : {
  4. "constant_score" : {
  5. "filter" : {
  6. "term" : {
  7. "price" : 20
  8. }
  9. }
  10. }
  11. }
  12. }

内部过滤器的操作

在内部,Elasticsearch 会在运行非评分查询的时执行多个操作: 深入搜索 - 图1为了实现以上设想,Elasticsearch 会为每个索引跟踪保留查询使用的历史状态。如果查询在最近的 256 次查询中会被用到,那么它就会被缓存到内存中。当 bitset 被缓存后,缓存会在那些低于 10,000 个文档(或少于 3% 的总索引数)的段(segment)中被忽略。这些小的段即将会消失,所以为它们分配缓存是一种浪费。
这些 bitsets 缓存是“智能”的:它们以增量方式更新。当我们索引新文档时,只需将那些新文档加入已有 bitset,而不是对整个缓存一遍又一遍的重复计算。和系统其他部分一样,过滤器是实时的,我们无需担心缓存过期问题。
一旦缓存了,非评分计算的 bitset 会一直驻留在缓存中直到它被剔除。剔除规则是基于 LRU 的:一旦缓存满了,最近最少使用的过滤器会被剔除

组合过滤器

tg:
sql:

  1. SELECT product
  2. FROM products
  3. WHERE (price = 20 OR productID = "XHDK-A-1293-#fJ3")
  4. AND (price != 30)

es:

  1. GET /my_store/products/_search
  2. {
  3. "query" : {
  4. "filtered" : {
  5. "filter" : {
  6. "bool" : {
  7. "should" : [
  8. { "term" : {"price" : 20}},
  9. { "term" : {"productID" : "XHDK-A-1293-#fJ3"}}
  10. ],
  11. "must_not" : {
  12. "term" : {"price" : 30}
  13. }
  14. }
  15. }
  16. }
  17. }
  18. }

布尔过滤器

  1. {
  2. "bool" : {
  3. "must" : [],
  4. "should" : [],
  5. "must_not" : [],
  6. }
  7. }

must
所有的语句都 必须(must) 匹配,与 AND 等价。
must_not
所有的语句都 不能(must not) 匹配,与 NOT 等价。
should
至少有一个语句要匹配,与 OR 等价。

嵌套布尔过滤器

sql

  1. SELECT document
  2. FROM products
  3. WHERE productID = "KDKE-B-9947-#kL5" OR (productID = "JODL-X-1937-#pV7"
  4. AND price = 30 )

es:

  1. GET /my_store/products/_search
  2. {
  3. "query" : {
  4. "filtered" : {
  5. "filter" : {
  6. "bool" : {
  7. "should" : [
  8. { "term" : {"productID" : "KDKE-B-9947-#kL5"}},
  9. { "bool" : {
  10. "must" : [
  11. { "term" : {"productID" : "JODL-X-1937-#pV7"}},
  12. { "term" : {"price" : 30}}
  13. ]
  14. }}
  15. ]
  16. }
  17. }
  18. }
  19. }
  20. }

查找多个精确的值

具体做法

  1. 将之前的term -> terms
  2. 将term 的值改为数组。
    1. GET /my_store/products/_search
    2. {
    3. "query" : {
    4. "constant_score" : {
    5. "filter" : {
    6. "terms" : {
    7. "price" : [20, 30]
    8. }
    9. }
    10. }
    11. }
    12. }

    查询范围

    sql:
    1. SELECT document
    2. FROM products
    3. WHERE price BETWEEN 20 AND 40
    es:
    1. GET /my_store/products/_search
    2. {
    3. "query" : {
    4. "constant_score" : {
    5. "filter" : {
    6. "range" : {
    7. "price" : {
    8. "gte" : 20,
    9. "lt" : 40
    10. }
    11. }
    12. }
    13. }
    14. }
    15. }
    range 查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,可供组合的选项如下:
  • gt: > 大于(greater than)
  • lt: < 小于(less than)
  • gte: >= 大于或等于(greater than or equal to)
  • lte: <= 小于或等于(less than or equal to)

日期范围

  1. "range" : {
  2. "timestamp" : {
  3. "gt" : "2014-01-01 00:00:00",
  4. "lt" : "2014-01-07 00:00:00"
  5. }
  6. }

当使用它处理日期字段时, range 查询支持对 日期计算(date math) 进行操作,比方说,如果我们想查找时间戳在过去一小时内的所有文档:

  1. "range" : {
  2. "timestamp" : {
  3. "gt" : "now-1h"
  4. }
  5. }

处理null 值

查询不为空的使用exists

sql:

  1. SELECT tags
  2. FROM posts
  3. WHERE tags IS NOT NULL

es:

  1. GET /my_index/posts/_search
  2. {
  3. "query" : {
  4. "constant_score" : {
  5. "filter" : {
  6. "exists" : { "field" : "tags" }
  7. }
  8. }
  9. }
  10. }

查询为空的使用missing

sql:

  1. SELECT tags
  2. FROM posts
  3. WHERE tags IS NULL

es:

  1. GET /my_index/posts/_search
  2. {
  3. "query" : {
  4. "constant_score" : {
  5. "filter": {
  6. "missing" : { "field" : "tags" }
  7. }
  8. }
  9. }
  10. }

全文搜索

相关性(Relevance)
查询与其结果间的相关程度,并根据这种相关程度对结果排名的一种能力,这种计算方式可以是

  • TF/IDF 方法、
  • 地理位置邻近、
  • 模糊相似,

或其他的某些算法。
分析(Analysis)
它是将文本块——>>>> token 的一个过程
目的是为了(a)创建倒排索引以及(b)查询倒排索引

匹配查询

首先,我们使用 bulkAPI 创建一些新的文档和索引:

  1. DELETE /my_index
  2. PUT /my_index
  3. { "settings": { "number_of_shards": 1 }}
  4. POST /my_index/my_type/_bulk
  5. { "index": { "_id": 1 }}
  6. { "title": "The quick brown fox" }
  7. { "index": { "_id": 2 }}
  8. { "title": "The quick brown fox jumps over the lazy dog" }
  9. { "index": { "_id": 3 }}
  10. { "title": "The quick brown fox jumps over the quick dog" }
  11. { "index": { "_id": 4 }}
  12. { "title": "Brown fox brown dog" }

单个词查询

  1. GET /my_index/my_type/_search
  2. {
  3. "query": {
  4. "match": {
  5. "title": "QUICK!"
  6. }
  7. }
  8. }

执行上面这个 match 查询的步骤是: 深入搜索 - 图2结果:

  1. "hits": [
  2. {
  3. "_id": "1",
  4. "_score": 0.5, // 分数
  5. "_source": {
  6. "title": "The quick brown fox"
  7. }
  8. },
  9. {
  10. "_id": "3",
  11. "_score": 0.44194174,
  12. "_source": {
  13. "title": "The quick brown fox jumps over the quick dog"
  14. }
  15. },
  16. {
  17. "_id": "2",
  18. "_score": 0.3125,
  19. "_source": {
  20. "title": "The quick brown fox jumps over the lazy dog"
  21. }
  22. }
  23. ]

分析:

  • 文档 1 最相关,因为它的 title 字段更短,即 quick 占据内容的一大部分
  • 文档 3 比 文档 2 更具相关性,因为在文档 3 中 quick 出现了两次

多词查询

  1. GET /my_index/my_type/_search
  2. {
  3. "query": {
  4. "match": {
  5. "title": "BROWN DOG!"
  6. }
  7. }
  8. }

即任何文档只要 title 字段里包含 指定词项中的至少一个词 就能匹配,被匹配的词项越多,文档就越相关。

提供精度

  1. GET /my_index/my_type/_search
  2. {
  3. "query": {
  4. "match": {
  5. "title": {
  6. "query": "BROWN DOG!",
  7. "operator": "and"
  8. }
  9. }
  10. }
  11. }

控制精度

  1. GET /my_index/my_type/_search
  2. {
  3. "query": {
  4. "match": {
  5. "title": {
  6. "query": "quick brown dog",
  7. "minimum_should_match": "75%"
  8. }
  9. }
  10. }
  11. }


参数 minimum_should_match 的设置非常灵活,可以根据用户输入词项的数目应用不同的规则。完整的信息参考文档 https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html#query-dsl-minimum-should-match

组合查询

过滤器做二元判断:文档是否应该出现在结果中?但查询更精妙,它除了决定一个文档是否应该被包括在结果中+计算文档的 相关程度
与过滤器一样, bool 查询也可以接受 must 、 must_not 和 should 参数下的多个查询语句。比如:

  1. GET /my_index/my_type/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": { "match": { "title": "quick" }},
  6. "must_not": { "match": { "title": "lazy" }},
  7. "should": [
  8. { "match": { "title": "brown" }},
  9. { "match": { "title": "dog" }}
  10. ]
  11. }
  12. }
  13. }

评分计算

bool 查询会为每个文档计算相关度评分 _score ,再将所有匹配的 must 和 should 语句的分数 _score 求和,最后除以 must 和 should 语句的总数。

must_not 语句不会影响评分;它的作用只是将不相关的文档排除。

控制精度

  1. GET /my_index/my_type/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "should": [
  6. { "match": { "title": "brown" }},
  7. { "match": { "title": "fox" }},
  8. { "match": { "title": "dog" }}
  9. ],
  10. "minimum_should_match": 2
  11. }
  12. }
  13. }

查询语句提升权重

假设想要查询关于 “full-text search(全文搜索)” 的文档,但我们希望为提及 “Elasticsearch” 或 “Lucene” 的文档给予更高的 权重 ,这里 更高权重 是指如果文档中出现 “Elasticsearch” 或 “Lucene” ,它们会比没有的出现这些词的文档获得更高的相关度评分 _score ,也就是说,它们会出现在结果集的更上面。

一个简单的 bool 查询 允许我们写出如下这种非常复杂的逻辑:

  1. GET /_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": {
  6. "match": {
  7. "content": {
  8. "query": "full text search",
  9. "operator": "and"
  10. }
  11. }
  12. },
  13. "should": [
  14. { "match": { "content": "Elasticsearch" }},
  15. { "match": { "content": "Lucene" }}
  16. ]
  17. }
  18. }
  19. }

我们可以通过指定 boost 来控制任何查询语句的相对的权重, boost 的默认值为 1 ,大于 1 会提升一个语句的相对权重。所以下面重写之前的查询:

  1. GET /_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": {
  6. "match": {
  7. "content": {
  8. "query": "full text search",
  9. "operator": "and"
  10. }
  11. }
  12. },
  13. "should": [
  14. { "match": {
  15. "content": {
  16. "query": "Elasticsearch",
  17. "boost": 3
  18. }
  19. }},
  20. { "match": {
  21. "content": {
  22. "query": "Lucene",
  23. "boost": 2
  24. }
  25. }}
  26. ]
  27. }
  28. }
  29. }

多字段搜索

多字符串搜索

tg: 用标题和作者去搜索对应的文章

  1. GET /_search
  2. {
  3. "query": {
  4. "bool": {
  5. "should": [
  6. { "match": { "title": "War and Peace" }},
  7. { "match": { "author": "Leo Tolstoy" }}
  8. ]
  9. }
  10. }
  11. }

可以用 bool 查询来包裹组合任意其他类型的查询,甚至包括其他的 bool 查询。我们可以在上面的示例中添加一条语句来指定译者版本的偏好:

  1. GET /_search
  2. {
  3. "query": {
  4. "bool": {
  5. "should": [
  6. { "match": { "title": "War and Peace" }},
  7. { "match": { "author": "Leo Tolstoy" }},
  8. {"bool": {
  9. "should": [
  10. { "match": { "translator": "Constance Garnett" }},
  11. { "match": { "translator": "Louise Maude" }}
  12. ]
  13. }}
  14. ],
  15. }
  16. }
  17. }

用图片来描述下上述计算过程 深入搜索 - 图3

语句的优先级

前例中每条语句贡献三分之一评分的这种方式可能并不是我们想要的,我们可能对 title 和 author 两条语句更感兴趣,这样就需要调整查询,使 title 和 author 语句相对来说更重要。

  1. GET /_search
  2. {
  3. "query": {
  4. "bool": {
  5. "should": [
  6. { "match": {
  7. "title": {
  8. "query": "War and Peace",
  9. "boost": 2
  10. }}},
  11. { "match": {
  12. "author": {
  13. "query": "Leo Tolstoy",
  14. "boost": 2
  15. }}},
  16. { "bool": {
  17. "should": [
  18. { "match": { "translator": "Constance Garnett" }},
  19. { "match": { "translator": "Louise Maude" }}
  20. ]
  21. }}
  22. ]
  23. }
  24. }
  25. }

获取 boost 参数 “最佳” 值,较为简单的方式就是不断尝试。
boost 值比较合理的区间处于 1 到 10 之间

单字符串查询(百度查询原理)

bool 适用场景,多条件,多字段映射查询。
但是,当我们输入单个字符串的时候,后怎么样呢?有以下三种情况

  1. 最佳字段
  2. 多数字段
  3. 混合字段

下面将这几种情况,分别介绍:

最佳字段

假设有个网站允许用户搜索博客的内容,以下面两篇博客内容文档为例:

  1. PUT /my_index/my_type/1
  2. {
  3. "title": "Quick brown rabbits",
  4. "body": "Brown rabbits are commonly seen."
  5. }
  6. PUT /my_index/my_type/2
  7. {
  8. "title": "Keeping pets healthy",
  9. "body": "My quick brown fox eats rabbits on a regular basis."
  10. }

用户输入词组 “Brown fox” 然后点击搜索按钮, 尝试猜想一下,是不是文档2的评分会更高一些呢?
让我们看下结果
image.png
看到上图结果,文档1的评分竟然比文档2高?
下面我们来分析下原因:
先回顾下bool查询的流程: 深入搜索 - 图5文档 1 的两个字段都包含 brown 这个词,所以两个 match 语句都能成功匹配并且有一个评分。文档 2 的 body 字段同时包含 brown 和 fox 这两个词,但 title 字段没有包含任何词。这样, body 查询结果中的高分,加上 title 查询中的 0 分,然后乘以二分之一,就得到比文档 1 更低的整体评分。

在本例中, title 和 body 字段是相互竞争的关系,所以就需要找到单个 最佳匹配 的字段。

那如果要最佳匹配 字段的评分作为查询的整体评分,要怎么做呢

dis_max 查询

全称:分离 最大化查询(Disjunction Max Query)
指将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回

  1. {
  2. "query": {
  3. "dis_max": {
  4. "queries": [
  5. { "match": { "title": "Brown fox" }},
  6. { "match": { "body": "Brown fox" }}
  7. ]
  8. }
  9. }
  10. }

得到我们想要的结果为:

  1. {
  2. "hits": [
  3. {
  4. "_id": "2",
  5. "_score": 0.21509302,
  6. "_source": {
  7. "title": "Keeping pets healthy",
  8. "body": "My quick brown fox eats rabbits on a regular basis."
  9. }
  10. },
  11. {
  12. "_id": "1",
  13. "_score": 0.12713557,
  14. "_source": {
  15. "title": "Quick brown rabbits",
  16. "body": "Brown rabbits are commonly seen."
  17. }
  18. }
  19. ]
  20. }

最佳字段查询调优

一个简单的 dis_max 查询会采用单个最佳匹配字段,而忽略其他的匹配:

  1. {
  2. "query": {
  3. "dis_max": {
  4. "queries": [
  5. { "match": { "title": "Quick pets" }},
  6. { "match": { "body": "Quick pets" }}
  7. ]
  8. }
  9. }
  10. }

看下结果

  1. {
  2. "hits": [
  3. {
  4. "_id": "1",
  5. "_score": 0.12713557,
  6. "_source": {
  7. "title": "Quick brown rabbits",
  8. "body": "Brown rabbits are commonly seen."
  9. }
  10. },
  11. {
  12. "_id": "2",
  13. "_score": 0.12713557,
  14. "_source": {
  15. "title": "Keeping pets healthy",
  16. "body": "My quick brown fox eats rabbits on a regular basis."
  17. }
  18. }
  19. ]
  20. }

不难发现,他们的评分是一样的

期望同时匹配 title 和 body 字段的文档比只与一个字段匹配的文档的相关度更高,但事实并非如此,因为 dismax 查询只会简单地使用 单个_ 最佳匹配语句的评分 _score 作为整体评分。

tie_breaker 参数

通过tie_breaker 参数也将其他匹配语句的评分也会考虑到

  1. {
  2. "query": {
  3. "dis_max": {
  4. "queries": [
  5. { "match": { "title": "Quick pets" }},
  6. { "match": { "body": "Quick pets" }}
  7. ],
  8. "tie_breaker": 0.3
  9. }
  10. }
  11. }

结果如下:

  1. {
  2. "hits": [
  3. {
  4. "_id": "2",
  5. "_score": 0.14757764,
  6. "_source": {
  7. "title": "Keeping pets healthy",
  8. "body": "My quick brown fox eats rabbits on a regular basis."
  9. }
  10. },
  11. {
  12. "_id": "1",
  13. "_score": 0.124275915,
  14. "_source": {
  15. "title": "Quick brown rabbits",
  16. "body": "Brown rabbits are commonly seen."
  17. }
  18. }
  19. ]
  20. }

终于是我们想要的结果了。

tie_breaker 可以是 0 到 1 之间的浮点数,其中 0 代表使用 dis_max 最佳匹配语句的普通逻辑, 1 表示所有匹配语句同等重要。最佳的精确值需要根据数据与查询调试得出,但是合理值应该与零接近(处于 0.1 - 0.4 之间),这样就不会颠覆 dis_max 最佳匹配性质的根本。

multi_match 查询

multi_match 查询为能在多个字段上反复执行相同查询提供了一种便捷方式。

best_fields 最佳字段
most_fields 多数字段
cross_fields 跨字段

默认情况下,查询的类型是 best_fields ,这表示它会为每个字段生成一个 match 查询,然后将它们组合到 dis_max 查询的内部,如下:

  1. {
  2. "dis_max": {
  3. "queries": [
  4. {
  5. "match": {
  6. "title": {
  7. "query": "Quick brown fox",
  8. "minimum_should_match": "30%"
  9. }
  10. }
  11. },
  12. {
  13. "match": {
  14. "body": {
  15. "query": "Quick brown fox",
  16. "minimum_should_match": "30%"
  17. }
  18. }
  19. },
  20. ],
  21. "tie_breaker": 0.3
  22. }
  23. }

用 multi_match 重写成更简洁的形式:

  1. {
  2. "multi_match": {
  3. "query": "Quick brown fox",
  4. "type": "best_fields",
  5. "fields": [ "title", "body" ],
  6. "tie_breaker": 0.3,
  7. "minimum_should_match": "30%"
  8. }
  9. }

查询的字段名的模糊匹配

  1. {
  2. "multi_match": {
  3. "query": "Quick brown fox",
  4. "fields": "*_title"
  5. }
  6. }

提升单个字段的权重

可以使用 ^ 字符语法为单个字段提升权重,在字段名称的末尾添加 ^boost ,其中 boost 是一个浮点数:

  1. {
  2. "multi_match": {
  3. "query": "Quick brown fox",
  4. "fields": [ "*_title", "chapter_title^2" ]
  5. }
  6. }

多数字段

全文搜索被称作是 召回率(Recall)精确率(Precision) 的战场:
召回率 ——返回所有的相关文档;
精确率 ——不返回无关文档。

提高全文相关性精度的常用方式是为同一文本建立多种方式的索引,每种方式都提供了一个不同的相关度信号 signal

文档同时又与 signal 信号字段匹配,那么它会获得额外加分

多字段映射

首先要做的事情就是对我们的字段索引两次:一次使用词干模式以及一次非词干模式

  1. DELETE /my_index
  2. PUT /my_index
  3. {
  4. "settings": { "number_of_shards": 1 },
  5. "mappings": {
  6. "my_type": {
  7. "properties": {
  8. "title": {
  9. "type": "string",
  10. "analyzer": "english",
  11. "fields": {
  12. "std": {
  13. "type": "string",
  14. "analyzer": "standard"
  15. }
  16. }
  17. }
  18. }
  19. }
  20. }
  21. }
  22. PUT /my_index/my_type/1
  23. { "title": "My rabbit jumps" }
  24. PUT /my_index/my_type/2
  25. { "title": "Jumping jack rabbits" }

这里用一个简单 match 查询 title 标题字段是否包含 jumping rabbits (跳跃的兔子):

  1. GET /my_index/_search
  2. {
  3. "query": {
  4. "match": {
  5. "title": "jumping rabbits"
  6. }
  7. }
  8. }

因为有了 english 分析器,这个查询是在查找以 jump 和 rabbit 这两个被提取词的文档。两个文档的 title 字段都同时包括这两个词,所以两个文档得到的评分也相同:

  1. {
  2. "hits": [
  3. {
  4. "_id": "1",
  5. "_score": 0.42039964,
  6. "_source": {
  7. "title": "My rabbit jumps"
  8. }
  9. },
  10. {
  11. "_id": "2",
  12. "_score": 0.42039964,
  13. "_source": {
  14. "title": "Jumping jack rabbits"
  15. }
  16. }
  17. ]
  18. }