文章目录
- 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.keyword
,articleID.keyword
默认不分词,会最多保留256个字符。
2. term查询
我们首先来看最为常用的 term
查询, 可以用它处理数字(numbers)、布尔值(Booleans)、日期(dates)以及文本(text)。
1、term查询数字
通常当查找一个精确值的时候,我们不希望对查询进行评分计算。只希望对文档进行包括或排除的计算,所以我们会使用 constant_score
查询以非评分模式来执行 term
查询并以1作为统一评分。
最终组合的结果是一个 constant_score
查询,它包含一个 term
查询, 使用 constant_score
将 term
查询转化成为过滤器
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"
}
}
]
}
}
]
}
}
}
}
}
term
和 bool
过滤器是兄弟关系,他们都处于外层的布尔逻辑 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
查询对于查找单个值非常有用,但通常我们可能想搜索多个值。 如果我们想要搜索articleID
为KDKE-B-9947-#kL5
或QQPX-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"
}
}
]
}
}
注意:非常重要!包含,而不是相等
一定要了解 term
和 terms
是包含(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 |
|
|
|
|
|
|
当 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
}
}
]
}
}