原创

Elasticsearch基础(十)——Term Filter

本章,我将通过示例讲解下Elasticsearch中Term Filter的使用。所谓Term Filter,就是不会对搜索关键词进行分词操作,根据exact value进行搜索,数字、boolean、date天然支持这种方式。但是对于text类型的字段需要特别注意,ES默认会对text字段先分词,再检索。

一、案例实战

1.1 数据准备

先批量插入一些论坛帖子信息,索引为forum

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" }

mapping结构

我们看下这个索引的mapping:

#请求:
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"
        }
      }
    }
  }
}

可以看到postDate默认就是date类型。这里关键说下articleID,它的类型是text,Elasticsearch默认会对text类型的字段进行分词,建立倒排索引;其次,还会生成一个keyword字段,这个keyword就是articleID的内容,不会分词,用于建立正排索引,上述256的意思是:如果内容过长,只保留256个字符。

我们通过_analyze命令来理解下,先看下默认的articleID字段:

#请求:分析下aticleID的默认分词结果
GET /forum/_analyze
{
  "field": "articleID",
  "text":"XHDK-A-1293-#fJ3" 
}

#响应:可以看到"XHDK-A-1293-#fJ3"其实被normalization了
{
  "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
    }
  ]
}

再来看下articleID.keyword字段:

#请求:分析下articleID.keyword的默认分词结果
GET /forum/_analyze
{
  "field": "articleID.keyword",
  "text":"XHDK-A-1293-#fJ3"

}
#响应:可以看到"XHDK-A-1293-#fJ3"压根没被分词,原样返回了
{
  "tokens" : [
    {
      "token" : "XHDK-A-1293-#fJ3",
      "start_offset" : 0,
      "end_offset" : 16,
      "type" : "word",
      "position" : 0
    }
  ]
}

1.2 term filter示例

接着,我们来看下如何使用Term Filter。首先看一个示例:根据帖子ID搜索帖子。由于用了term filter语法,所以不会对搜索关键字进行分词:

#请求:我们不关心相关度分数,所以用了constant_score,将相关度分数置为1
GET /forum/_search
{
    "query" : {
        "constant_score" : { 
            "filter" : {
                "term" : { 
                    "articleID" : "XHDK-A-1293-#fJ3"
                }
            }
        }
    }
}
#响应:因为搜索关键字"XHDK-A-1293-#fJ3"不分词,而articleID这个字段本身是分词的,所以查不出结果
{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

我们直接对articleID进行term filter是查不出结果的,需要使用articleID.keyword:

#请求:对articleID.keyword进行term filter
GET /forum/_search
{
    "query" : {
        "constant_score" : { 
            "filter" : {
                "term" : { 
                    "articleID.keyword" : "XHDK-A-1293-#fJ3"
                }
            }
        }
    }
}
#响应:因为articleID.keyword保存在完整text内容,所以可以匹配到
{
  "took" : 0,
  "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" : "article",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "articleID" : "XHDK-A-1293-#fJ3",
          "userID" : 1,
          "hidden" : false,
          "postDate" : "2017-01-01"
        }
      }
    ]
  }
}

二、Filter底层原理

2.1 匹配document

term filter在执行时,首先会在倒排索引中匹配filter条件,也就是我们的搜索关键字,用于获取document list。我们以postDatge来举个例子,如果查找“2017-02-02”,会发现”2017-02-02“对应的document list是doc2、doc3:

word doc1 doc2 doc3
2017-01-01 Y Y N
2017-02-02 N Y Y
2017-03-03 Y Y Y

2.2 构建bitset

接着,为每个fiter条件构建一个bitset。bitset就是一个二进制的数组,数组每个元素都是0或1,用来标识某个doc对这个filter条件是否匹配,匹配就是1,否则为0。比如,doc1不匹配"2017-02-02",而doc2和do3是匹配的,所以"2017-02-02"这个filter条件的bitset就是[0, 1, 1]。

2.3 遍历bitset

由于在一个search请求中,可以有多个filter条件,而filter条件都会对应一个bitset。所以这一步,ES会从最稀疏的bitset开始遍历,优先过滤掉尽可能多的数据。比如我们的filter条件是postDate=2017-01-01,userID=1,对应的bitset是:
postDate: [0, 0, 1, 1, 0, 0]
userID: [0, 1, 0, 1, 0, 1]

那么遍历完两个bitset之后,找到匹配所有filter条件只有doc4,就将其作为结果返回给client了。

2.4 缓存bitset

Elasticsearch会将一些频繁访问的filter条件和它对应的bitset缓存在内存中,这样就可以提高检索效率了。

注意,如果document保存在某个很小的segment上的话(segment记录数<1000,或segment大小<index总大小的3%),Elasticsearch就不会对其缓存。因为segment很小的话,会在后台被自动合并,那么缓存也没有什么意义了,因问segment很快就消失了。

这里就可以看出,filter为什么比query的性能更好了,filter除了不需要计算相关度分数并按其排序外,filter还会缓存检索结果对应的bitset。

2.5 bitset更新

如果document有新增或修改,那么filter条件对应的cached bitset会被自动更新。举个例子,假设postDate=2017-01-01对应的bitset为[0, 0, 1, 0]。

  • 如果新增一条doc5:id=5,postDate=2017-01-01,那postDate=2017-01-01这个filter的bitset会全自动更新成[0, 0, 1, 0, 1];
  • 如果修改doc1:id=1,postDate=2016-12-30,那postDate=2016-01-01这个filter的bitset会全自动更新成[1, 0, 1, 0, 1];

三、总结

本章,我介绍了term filter的基本使用和底层的bitset原理。关于term filter的更多API,读者可以参考官方文档,本系统的目的是介绍Elasticsearch中各种检索功能的基本使用,重点关注的是底层的实现原理。

正文到此结束
本文目录