文章目录

  • 1. 结构化搜索 [filter]
  • 2. term查询
  • 3. filter过滤器的执行原理
  • 4. 组合过滤器
  • 5. terms查询
  • 6. range查询


1. 结构化搜索 [filter]

结构化搜索是指有关探询那些具有内在结构数据的过程。比如日期、时间和数字都是结构化的:它们有精确的格式,我们可以对这些格式进行逻辑操作。比较常见的操作包括比较数字或时间的范围,或判定两个值的大小。(其实就是指那些不可以被分词的数据)

文本也可以是结构化的。如彩色笔可以有离散的颜色集合: 红(red)绿(green)蓝(blue) 。一个博客可能被标记了关键词 分布式(distributed)搜索(search) 。 (这些词都不可以被分词)

在结构化查询中,我们得到的结果总是非是即否,要么存于集合之中,要么存在集合之外。结构化查询不关心文件的相关度或评分;它简单的对文档包括或排除处理。

这在逻辑上是能说通的,因为一个数字不能比其他数字更适合存于某个相同范围。结果只能是:存于范围之中,抑或反之。同样,对于结构化文本来说,一个值要么相等,要么不等。没有更似这种概念。

当进行精确值查找时, 我们会使用过滤器(filters)。过滤器很重要,因为它们执行速度非常快,不会计算相关度(直接跳过了整个评分阶段)而且很容易被缓存。

1、首先构造测试数据:

POST /forum/_bulk
{ "index": { "_id": 1 }}
{ "articleID" : "XHDK-A-1293-#fJ3", "userID" : 1, "hidden": false, "postDate": "2017-01-01" }
{ "index": { "_id": 2 }}
{ "articleID" : "KDKE-B-9947-#kL5", "userID" : 1, "hidden": false, "postDate": "2017-01-02" }
{ "index": { "_id": 3 }}
{ "articleID" : "JODL-X-1937-#pV7", "userID" : 2, "hidden": false, "postDate": "2017-01-01" }
{ "index": { "_id": 4 }}
{ "articleID" : "QQPX-R-3956-#aD8", "userID" : 2, "hidden": true, "postDate": "2017-01-02" }

初步来说,就先搞4个字段,因为整个es是支持json document格式的,所以说扩展性和灵活性非常之好。如果后续随着业务需求的增加,要在document中增加更多的field,那么我们可以很方便的随时添加field。但是如果是在关系型数据库中,比如mysql,我们建立了一个表,现在要给表中新增一些column,那就很坑爹了,必须用复杂的修改表结构的语法去执行。而且可能对系统代码还有一定的影响。

2、查询索引的映射:

GET /forum/_mapping
{
  "forum" : {
    "mappings" : {
      "properties" : {
        "articleID" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "hidden" : {
          "type" : "boolean"
        },
        "postDate" : {
          "type" : "date"
        },
        "userID" : {
          "type" : "long"
        }
      }
    }
  }
}

type=text,默认会设置两个field,一个是field本身,比如articleID,就是分词的;还有一个的话,就是field.keywordarticleID.keyword默认不分词,会最多保留256个字符。

2. term查询

我们首先来看最为常用的 term 查询, 可以用它处理数字(numbers)、布尔值(Booleans)、日期(dates)以及文本(text)。

1、term查询数字

通常当查找一个精确值的时候,我们不希望对查询进行评分计算。只希望对文档进行包括或排除的计算,所以我们会使用 constant_score 查询以非评分模式来执行 term 查询并以1作为统一评分。

最终组合的结果是一个 constant_score 查询,它包含一个 term 查询, 使用 constant_scoreterm 查询转化成为过滤器

GET /forum/_search
{
  "query":{
    "constant_score":{
      "filter":{
        "term":{
          "userID":1
        }
      }
    }
  }
}

2、term查询文本

使用 term 查询匹配字符串和匹配数字一样容易。如果我们想要查询某个具体aticleID对应的文档:

GET /forum/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "articleID": "XHDK-A-1293-#fJ3"
        }
      }
    }
  }
}
{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

可以看到查询结果为null,无法获得期望的结果,为什么呢?问题不在 term 查询,而在于索引数据的方式。 如果我们使用 analyze API,我们可以看到这里的 aticleID被拆分成多个更小的 token :

GET /forum/_analyze
{
  "field": "articleID",
  "text":"XHDK-A-1293-#fJ3"
}
{
  "tokens" : [
    {
      "token" : "xhdk",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "a",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "1293",
      "start_offset" : 7,
      "end_offset" : 11,
      "type" : "<NUM>",
      "position" : 2
    },
    {
      "token" : "fj3",
      "start_offset" : 13,
      "end_offset" : 16,
      "type" : "<ALPHANUM>",
      "position" : 3
    }
  ]
}

这里有几点需要注意:

  • Elasticsearch 用 4 个不同的 token 而不是单个 token 来表示这个 articleID。
  • 所有字母都是小写的。
  • 丢失了连字符和哈希符( # )。

所以当我们用 term 查找精确值 XHDK-A-1293-#fJ3 的时候,找不到任何文档,因为它并不在我们的倒排索引中,正如前面呈现出的分析结果,索引里有四个 token ,也就是说是将articleID分词后建立的倒排索引。

显然这种对 ID 码或其他任何精确值的处理方式并不是我们想要的。为了避免这种问题,我们需要告诉 ES 该字段具有精确值,不需要进行分词,因为term查询也不会对搜索关键词进行分词。为了修正搜索结果,我们需要首先删除旧索引(因为它的映射不再正确)然后创建一个能正确映射的新索引。

① 删除索引是必须的,因为我们不能更新已存在的映射。

# 删除一个索引
DELETE /forum

② 在索引被删除后,我们可以创建新的索引并为其指定自定义映射,告诉 Elasticsearch ,我们不想对 articleID 做任何分析。

# 重新创建一个索引,并指定articleID的type为keyword
PUT /forum
{
  "mappings": {
    "properties": {
      "articleID": {
        "type": "keyword"
      }
    }
  }
}

③ 为文档重建索引:

# 添加数据
POST /forum/_bulk
{ "index": { "_id": 1 }}
{ "articleID" : "XHDK-A-1293-#fJ3", "userID" : 1, "hidden": false, "postDate": "2017-01-01" }
{ "index": { "_id": 2 }}
{ "articleID" : "KDKE-B-9947-#kL5", "userID" : 1, "hidden": false, "postDate": "2017-01-02" }
{ "index": { "_id": 3 }}
{ "articleID" : "JODL-X-1937-#pV7", "userID" : 2, "hidden": false, "postDate": "2017-01-01" }
{ "index": { "_id": 4 }}
{ "articleID" : "QQPX-R-3956-#aD8", "userID" : 2, "hidden": true, "postDate": "2017-01-02" }

④ 此时, term 查询就能搜索到我们想要的结果,让我们再次搜索新索引过的数据(注意,查询和过滤并没有发生任何改变,改变的是数据映射的方式):

GET /forum/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "articleID": "XHDK-A-1293-#fJ3"
        }
      }
    }
  }
}

查询到了我们想要的数据:因为 articleID 字段是未分析过的, term 查询不会对其做任何分析,查询会进行精确查找并返回文档 1 。成功!

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "forum",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "XHDK-A-1293-#fJ3",
          "userID" : 1,
          "hidden" : false,
          "postDate" : "2017-01-01"
        }
      }
    ]
  }
}

3. filter过滤器的执行原理

1、在倒排索引中查找搜索串,查找匹配文档

例如,根据日期字段建立的倒排序索引:

word		doc1		doc2		doc3

2017-01-01	  *			  *
2017-02-02				  *			  *
2017-03-03	  *			  *			  *

现在想根据term查询在倒排索引中查找2017-02-02,然后获取包含该 term 的所有文档,发现doc2,doc3满足我们的要求。

2、构建bitset

过滤器为每个在倒排索引中搜索到的结果,构建一个bitset(一个包含 0 和 1 的数组),它描述了哪个文档会包含该 term 。匹配文档的标志位是 1 。因为在本例中bitset的值为[0,1,1]

3、遍历 bitset

其实可以在一个search请求中,一次性发出多个filter条件,每个filter条件都会对应一个bitset,一旦为每个查询生成了 bitset ,Elasticsearch 就会遍历所有的bitset 从而找到满足所有过滤条件的匹配文档的集合。遍历每个filter条件对应的bitset时,一般先从最稀疏的bitset开始遍历(因为它可以排除掉大量的文档),从而快速查找满足所有条件的document。

那么什么是稀疏数组呢?一般是数组中0比较多而1比较少的数组,比如下面的例子:

[0, 0, 0, 1, 0, 0]:比较稀疏
[0, 1, 0, 1, 0, 1]

假如term请求为查询出postDate=2017-01-01,userID=1对应的文档,那么此时会生成两个bitset:

doc1   doc2  doc3  doc4  doc5   doc6 
postDate:    [  0,    0,    1,    1,    0,     0    ]
userID:      [  0,    1,    0,    1,    0,     1    ]

先遍历比较稀疏的postDate对应的bitset,然后遍历userID对应的bitset,遍历完两个bitset之后,找到匹配所有条件的doc就是doc4(bitset都为1,说明两个过滤条件都匹配)

此时,将文档查询到之后就可以将document作为结果返回给client了

4、增量使用计数

ES能够缓存非评分查询从而获取更快的访问,比如postDate=2017-01-01,[0, 0, 1, 1, 0, 0],可以缓存在内存中,这样下次如果再有这个条件过来的时候,就不用重新扫描倒排索引,反复生成bitset,可以大幅度提升性能。但是它也会不太聪明地缓存一些使用极少的东西。非评分计算因为倒排索引已经足够快了,所以我们只想缓存那些我们知道在将来会被再次使用的查询,以避免资源的浪费。为了实现以上设想,Elasticsearch 会为每个索引跟踪保留查询使用的历史状态。如果查询在最近的 256 次查询中会被用到,那么它就会被缓存到内存中。

当 bitset 被缓存后,缓存会在那些低于 10,000 个文档(或少于 3% 的总索引数)的段(segment)中被忽略。这些小的段即将会消失,所以为它们分配缓存是一种浪费。segment数据量很小,会在后台自动合并,小segment很快就会跟其他小segment合并成大segment,此时就缓存也没有什么意义,segment很快就消失了。

4. 组合过滤器

前面的两个例子都是单个过滤器(filter)的使用方式。 在实际应用中,我们很有可能会过滤多个值或字段。比方说,搜索发帖日期为2017-01-01,或者帖子ID为XHDK-A-1293-#fJ3的帖子,同时要求帖子的发帖日期绝对不为2017-01-02。怎样用 Elasticsearch 来表达下面的 SQL ?

select *
from forum 
where (post_date='2017-01-01' or article_id='XHDK-A-1293-#fJ3')
and post_date!='2017-01-02'

这种情况下,我们需要 bool (布尔)过滤器。 这是个复合过滤器 ,它可以接受多个其他过滤器作为参数,并将这些过滤器结合成各式各样的布尔(逻辑)组合。

1、布尔过滤器

一个 bool 过滤器由三部分组成:

{
   "bool" : {
      "must" :     [],
      "should" :   [],
      "must_not" : [],
   }
}

must:所有的语句都必须(must)匹配,与 AND 等价。

must_not:所有的语句都不能(must not)匹配,与 NOT 等价。

should:至少有一个语句要匹配,与 OR 等价。

就这么简单! 当我们需要多个过滤器时,只须将它们置入 bool 过滤器的不同部分即可。一个 bool 过滤器的每个部分都是可选的(例如,我们可以只有一个 must 语句),而且每个部分内部可以只有一个或一组过滤器。

用 Elasticsearch 来表示本部分开始处的 SQL 例子,将两个 term 过滤器置入 bool 过滤器的 should 语句内,再增加一个语句处理 NOT 非的条件:

# 在 should 语句块里面的两个 term 过滤器与 bool 过滤器是父子关系,两个 term 条件需要匹配其一
GET /forum/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "bool": {
          "should": [
            {"term": { "postDate": "2017-01-01" }},
            {"term": {"articleID": "XHDK-A-1293-#fJ3"}}
          ],
          "must_not": {
            "term": {
              "postDate": "2017-01-02"
            }
          }
        }
      }
    }
  }
}

我们搜索的结果返回了 2 个命中结果,两个文档分别匹配了 bool 过滤器其中的一个条件:

{
  "took" : 26,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "forum",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "XHDK-A-1293-#fJ3",
          "userID" : 1,
          "hidden" : false,
          "postDate" : "2017-01-01"
        }
      },
      {
        "_index" : "forum",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "JODL-X-1937-#pV7",
          "userID" : 2,
          "hidden" : false,
          "postDate" : "2017-01-01"
        }
      }
    ]
  }
}

2、嵌套布尔过滤器

尽管 bool 是一个复合的过滤器,可以接受多个子过滤器,需要注意的是 bool 过滤器本身仍然还只是一个过滤器。 这意味着我们可以将一个 bool 过滤器置于其他 bool 过滤器内部,这为我们提供了对任意复杂布尔逻辑进行处理的能力。

搜索帖子ID为XHDK-A-1293-#fJ3,或者是帖子ID为JODL-X-1937-#pV7且发帖日期为2017-01-01的帖子

select *
from forum 
where article_id='XHDK-A-1293-#fJ3'
or (article_id='JODL-X-1937-#pV7' and post_date='2017-01-01')
GET /forum/_search 
{
  "query": {
    "constant_score": {
      "filter": {
        "bool": {
          "should": [
            {
              "term": {
                "articleID": "XHDK-A-1293-#fJ3"
              }
            },
            {
              "bool": {
                "must": [
                  {
                    "term":{
                      "articleID": "JODL-X-1937-#pV7"
                    }
                  },
                  {
                    "term": {
                      "postDate": "2017-01-01"
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
  }
}

termbool 过滤器是兄弟关系,他们都处于外层的布尔逻辑 should 的内部,返回的命中文档至少须匹配其中一个过滤器的条件。

bool 过滤器中的这两个 term 语句作为兄弟关系,同时处于 must 语句之中,所以返回的命中文档要必须都能同时匹配这两个条件。

得到的结果有两个文档,它们各匹配 should 语句中的一个条件:

{
  "took" : 26,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "forum",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "XHDK-A-1293-#fJ3",
          "userID" : 1,
          "hidden" : false,
          "postDate" : "2017-01-01"
        }
      },
      {
        "_index" : "forum",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "JODL-X-1937-#pV7",
          "userID" : 2,
          "hidden" : false,
          "postDate" : "2017-01-01"
        }
      }
    ]
  }
}

5. terms查询

term 查询对于查找单个值非常有用,但通常我们可能想搜索多个值。 如果我们想要搜索articleIDKDKE-B-9947-#kL5QQPX-R-3956-#aD8的文档该如何处理呢?

不需要使用多个 term 查询,我们只要用单个 terms 查询, terms 查询好比是 term 查询的复数形式。它几乎与 term 的使用方式一模一样,与指定单个价格不同,我们只要将 term 字段的值改为数组即可:

term: {“field”: “value”}
terms: {“field”: [“value1”, “value2”]}

GET /forum/_search 
{
  "query": {
    "constant_score": {
      "filter": {
        "terms": {
          "articleID": [
            "KDKE-B-9947-#kL5",
            "QQPX-R-3956-#aD8"
          ]
        }
      }
    }
  }
}
{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "forum",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "KDKE-B-9947-#kL5",
          "userID" : 1,
          "hidden" : false,
          "postDate" : "2017-01-02"
        }
      },
      {
        "_index" : "forum",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "QQPX-R-3956-#aD8",
          "userID" : 2,
          "hidden" : true,
          "postDate" : "2017-01-02"
        }
      }
    ]
  }
}

注意:非常重要!包含,而不是相等

一定要了解 termterms 是包含(contains)操作,而非等值(equals)(判断)。 如何理解这句话呢?

1、首先构造数据,为帖子文档添加tag字段:

POST /forum/_bulk
{ "update": { "_id": "1"} }
{ "doc" : {"tag" : ["java", "hadoop"]} }
{ "update": { "_id": "2"} }
{ "doc" : {"tag" : ["java"]} }
{ "update": { "_id": "3"} }
{ "doc" : {"tag" : ["hadoop"]} }
{ "update": { "_id": "4"} }
{ "doc" : {"tag" : ["java", "elasticsearch"]} }

2、搜索tag中包含java的帖子:

GET /forum/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "terms" : { 
                    "tag" : ["java"]
                }
            }
        }
    }
}

他会匹配下面的3个文档:

{ "tag" : ["java","hadoop"] }
{ "tag" : ["java"] }
{ "tag" : [ "java","elasticsearch"] }

尽管第1和第3个文档包含除java以外的其他词,它还是被匹配并作为结果返回。

回忆一下 term 查询是如何工作的? Elasticsearch 会在倒排索引中查找包括某 term 的所有文档,然后构造一个 bitset 。在我们的例子中,倒排索引表如下:

Token

DocIDs

java

1,2,3

elasticsearch

3

hadoop

1

term 查询匹配标记 java 时,它直接在倒排索引中找到记录并获取相关的文档 ID,如倒排索引所示,这里文档1,2,3均包含该标记,所以3个文档会同时作为结果返回。

6. range查询

到目前为止,对于数字,只介绍如何处理精确值查询。实际上,对数字范围进行过滤有时会更有用。例如,我们可能想要查找所有搜索浏览量在30~60之间的帖子文档。

1、构造数据,为帖子数据增加浏览量字段:

POST /forum/_bulk
{ "update": { "_id": "1"} }
{ "doc" : {"view_cnt" : 30} }
{ "update": { "_id": "2"} }
{ "doc" : {"view_cnt" : 50} }
{ "update": { "_id": "3"} }
{ "doc" : {"view_cnt" : 100} }
{ "update": { "_id": "4"} }
{ "doc" : {"view_cnt" : 80} }

2、搜索浏览量在30~60之间的帖子文档:Elasticsearch 有 range 查询,不出所料地,可以用它来查找处于某个范围内的文档:

GET /forum/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "view_cnt": {
            "gt": 30,
            "lt": 60
          }
        }
      }
    }
  }
}

range 查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,可供组合的选项如下:

  • gt: > 大于(greater than)
  • lt: < 小于(less than)
  • gte: >= 大于或等于(greater than or equal to)
  • lte: <= 小于或等于(less than or equal to)

3、range 查询同样可以应用在日期字段上

GET /forum/_search 
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "postDate": {
            "gt": "2017-01-01",
            "lt": "2017-01-03"
          }
        }
      }
    }
  }
}

4、当使用它处理日期字段时, range 查询支持对日期计算(date math)进行操作。

比方说,如果我们想查找帖子发表日期在过去一小时内的所有帖子:

GET /forum/_search 
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "postDate": {
            "gt": "now-1h"
          }
        }
      }
    }
  }
}

5、日期计算还可以被应用到某个具体的时间,并非只能是一个像 now 这样的占位符。只要在某个日期后加上一个双管符号 (||) 并紧跟一个日期数学表达式就能做到。

搜索发帖日期晚于2016-12-10号的帖子:

GET /forum/_search 
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "postDate": {
            "gt": "2017-1-1||-30d"
          }
        }
      }
    }
  }
}

发帖日期晚于2017-1-1减30天的帖子 :

{
  "took" : 374,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "forum",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "DHJK-B-1395-#Ky5",
          "userID" : 3,
          "hidden" : false,
          "postDate" : "2017-03-01",
          "tag" : [
            "elasticsearch"
          ],
          "tag_cnt" : 1,
          "view_cnt" : 10
        }
      }
    ]
  }
}