目录
1.简介
结构化搜索是指查询包含内部结构的数据。文本可以被结构化。
日期,时间,和数字都是结构化的:
它们有明确的格式去执行逻辑操作。一般包括比较数字或日期的范围,或确定两个值哪个大。
通过结构化搜索,查询结果始终是 是或非;是否应该属于集合。结构化搜索不关心文档的相关性或分数,它只是简单的包含或排除文档。
这必须是有意义的逻辑,一个数字不能比同一个范围中的其他数字 更多。它只能包含在一个范围中 —— 或不在其中。类似的,对于结构化文本,一个值必须相等或不等。这里没有 更匹配 的概念。
1.1.查找准确值(效率高)
对于准确值,需要使用过滤器。过滤器的重要性在于它们非常的快。它们不计算相关性(避过所有计分阶段)而且很容易被缓存。
用于数字的 term 过滤器
这个过滤器旨在处理数字,布尔值,日期,和文本。
【举例】
POST /my_store/products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10, "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20, "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30, "productID" : "QQPX-R-3956-#aD8" }
GET /my_store/products/_search
{
"query" : {
"filtered" : { // filtered 查询同时接受 query 与 filter
"query" : {
"match_all" : {} // match_all 用来匹配所有文档,这是默认行为。
},
"filter" : {
"term" : { // 过滤器
"price" : 20
}
}
}
}
}
【结果】
"hits" : [
{
"_index" : "my_store",
"_type" : "products",
"_id" : "2",
"_score" : 1.0, // 过滤器不会执行计分和计算相关性,分值由match_all查询产生,所有文档一视同仁,所有每个结果的分值都是1
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5"
}
}
]
用于文本的 term 过滤器
【举例】
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"term" : {
"productID" : "XHDK-A-1293-#fJ3"
}
}
}
}
}
【结果】
没有得到任何结果值,问题在于数据被索引的方式。
GET /my_store/_analyze?field=productID
XHDK-A-1293-#fJ3
{
"tokens" : [ {
"token" : "xhdk",
"start_offset" : 0,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 1
}, {
"token" : "a",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 2
}, {
"token" : "1293",
"start_offset" : 7,
"end_offset" : 11,
"type" : "<NUM>",
"position" : 3
}, {
"token" : "fj3",
"start_offset" : 13,
"end_offset" : 16,
"type" : "<ALPHANUM>",
"position" : 4
} ]
}
【删除旧索引,并创建一个正确映射的索引】
DELETE /my_store // 必须首先删除索引,因为不能修改已经存在的映射
PUT /my_store // 删除后,可以用自定义的映射来创建它
{
"mappings" : {
"products" : {
"properties" : {
"productID" : {
"type" : "string",
"index" : "not_analyzed" // 这里表示不希望 productID 被分析
}
}
}
}
}
【结果】
POST /my_store/products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10, "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20, "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30, "productID" : "QQPX-R-3956-#aD8" }
【查询】
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"term" : {
"productID" : "XHDK-A-1293-#fJ3"
}
}
}
}
}
内部过滤操作
Elasticsearch 在内部会通过一些操作来执行一次过滤:
1. 查找匹配文档。
term 过滤器在倒排索引中查找词 XHDK-A-1293-#fJ3 ,然后返回包含那个词的文档列表。
2. 创建字节集
过滤器将创建一个 字节集 —— 一个由 1 和 0 组成的数组 —— 描述哪些文档包含这个词。
3. 缓存字节集
字节集被储存在内存中,以使能用它来跳过步骤 1 和 2。这大大的提升了性能,让过滤变得非常的快。
当执行 filtered 查询时,filter 会比 query 早执行。结果字节集会被传给 query 来跳过已经被排除的文档。这种过滤器提升性能的方式,查询更少的文档意味着更快的速度。
1.2.组合过滤
布尔过滤器
bool 过滤器由三部分组成:
{
"bool" : {
"must" : [],
"should" : [],
"must_not" : [],
}
}
must : 所有分句都必须匹配,与 AND 相同。
must_not : 所有分句都必须不匹配,与 NOT 相同。
should : 至少有一个分句匹配,与 OR 相同。
提示:
bool 过滤器的每个部分都是可选的(例如,你可以只保留一个 must 分句),而且每个部分可以包含一到多个过滤器
【举例】
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"bool" : {
"should" : [
{ "term" : {"price" : 20}},
{ "term" : {"productID" : "XHDK-A-1293-#fJ3"}} <2>
],
"must_not" : {
"term" : {"price" : 30}
}
}
}
}
}
}
【结果】
"hits" : [
{
"_id" : "1",
"_score" : 1.0,
"_source" : {
"price" : 10,
"productID" : "XHDK-A-1293-#fJ3" // 匹配
}
},
{
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5" // 匹配
}
}
]
嵌套布尔过滤器
【举例】
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"bool" : {
"should" : [
{ "term" : {"productID" : "KDKE-B-9947-#kL5"}}, // 条件1
{ "bool" : { // 条件 2
"must" : [
{ "term" : {"productID" : "JODL-X-1937-#pV7"}},
{ "term" : {"price" : 30}}
]
}}
]
}
}
}
}
}
【结果】
"hits" : [
{
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5" // 匹配条件1
}
},
{
"_id" : "3",
"_score" : 1.0,
"_source" : {
"price" : 30, // 匹配条件2
"productID" : "JODL-X-1937-#pV7" // 匹配条件2
}
}
]
1.3.查询多个准确值
【举例】
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"terms" : {
"price" : [20, 30]
}
}
}
}
}
【结果】
"hits" : [
{
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5"
}
},
{
"_id" : "3",
"_score" : 1.0,
"_source" : {
"price" : 30,
"productID" : "JODL-X-1937-#pV7"
}
},
{
"_id": "4",
"_score": 1.0,
"_source": {
"price": 30,
"productID": "QQPX-R-3956-#aD8"
}
}
]
1.4.包含,而不是相等
理解 term 和 terms 是包含操作,而不是相等操作,这点非常重要。
term 过滤器是怎么工作的: 它检查倒排索引中所有具有短语的文档,然后组成一个字节集。
当执行 term 过滤器来查询 search 时,它直接在倒排索引中匹配值并找出相关的 ID。
【举例】
{ "tags" : ["search"] }
{ "tags" : ["search", "open_source"] }
提示: 倒排索引的特性让完全匹配一个字段变得非常困难。
完全匹配
{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }
【举例】
GET /my_index/my_type/_search
{
"query": {
"filtered" : {
"filter" : {
"bool" : {
"must" : [
{ "term" : { "tags" : "search" } }, // 找出所有包含 search 短语的文档
{ "term" : { "tag_count" : 1 } } // 确保文档只有一个标签
]
}
}
}
}
}
这将匹配只有一个 search 标签的文档,而不是匹配所有包含了 search 标签的文档。
1.5.范围
"range" : {
"price" : {
"gt" : 20,
"lt" : 40
}
}
range 过滤器既能包含也能排除范围,通过下面的选项:
- gt : > 大于
- lt : < 小于
- gte : >= 大于或等于
- lte : <= 小于或等于
【举例】
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"range" : {
"price" : {
"gte" : 20,
"lt" : 40
}
}
}
}
}
}
日期范围
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-07 00:00:00"
}
}
当用于日期字段时, range 过滤器支持日期数学操作。
【举例】
"range" : {
"timestamp" : {
"gt" : "now-1h"
}
}
日期计算也能用于实际的日期,而不是仅仅是一个像 now 一样的占位符。只要在日期后加上双竖线 || ,就能使用日期数学表达式了。
"range" : {
"timestamp" : {
"gt" : "2019-01-01 00:00:00",
"lt" : "2019-01-01 00:00:00||+1M" // 早于 2019 年 1 月 1 号加一个月
}
}
字符串范围
字符串范围根据字典或字母顺序来计算。
提示:倒排索引中的短语按照字典顺序排序,也是为什么字符串范围使用这个顺序。
"range" : {
"title" : {
"gte" : "a",
"lt" : "b"
}
}
当心基数
为了在字符串上执行范围操作,Elasticsearch 会在这个范围内的每个短语执行 term 操作。
比日期或数字的范围操作慢得多。
字符串范围适用于一个基数较小的字段,一个唯一短语个数较少的字段。你的唯一短语数越多,搜索就越慢。
1.6.处理 Null 值
null, [](空数组)和 [null]
exists 过滤器
POST /my_index/posts/_bulk
{ "index": { "_id": "1" }}
{ "tags" : ["search"] } // tags 字段有一个值
{ "index": { "_id": "2" }}
{ "tags" : ["search", "open_source"] } // tags 字段有两个值
{ "index": { "_id": "3" }}
{ "other_field" : "some data" } // tags 字段不存在
{ "index": { "_id": "4" }}
{ "tags" : null } // tags 字段被设为 null
{ "index": { "_id": "5" }}
{ "tags" : ["search", null] } // tags 字段有一个值和一个 null
【举例】
GET /my_index/posts/_search
{
"query" : {
"filtered" : {
"filter" : {
"exists" : { "field" : "tags" }
}
}
}
}
【结果】
"hits" : [
{
"_id" : "1",
"_score" : 1.0,
"_source" : { "tags" : ["search"] }
},
{
"_id" : "5",
"_score" : 1.0,
"_source" : { "tags" : ["search", null] } // 这个字段存在是因为一个有值的标签被索引了,所以 null 对这个过滤器没有影响
},
{
"_id" : "2",
"_score" : 1.0,
"_source" : { "tags" : ["search", "open source"] }
}
]
missing 过滤器
【举例】
GET /my_index/posts/_search
{
"query" : {
"filtered" : {
"filter": {
"missing" : { "field" : "tags" }
}
}
}
}
【结果】
"hits" : [
{
"_id" : "3",
"_score" : 1.0,
"_source" : { "other_field" : "some data" }
},
{
"_id" : "4",
"_score" : 1.0,
"_source" : { "tags" : null }
}
]
什么时候 null 才表示 null
有时需要能区分一个字段是没有值,还是被设置为 null。可以将明确的 null 值用选择的占位符来代替
当指定字符串,数字,布尔值或日期字段的映射时,可以设置一个 null_value 来处理明确的 null 值。没有值的字段仍将被排除在倒排索引外。
当选定一个合适的 null_value 时,确保以下几点:
它与字段的类型匹配,不能在 date 类型的字段中使用字符串 null_value
它需要能与这个字段可能包含的正常值区分开来,以避免真实值和 null 值混淆
对象的 exists/missing
【文档】
{
"name" : {
"first" : "John",
"last" : "Smith"
}
}
【扁平化】
{
"name.first" : "John",
"name.last" : "Smith"
}
【举例】
{
"bool": {
"should": [
{ "exists": { "field": { "name.first" }}},
{ "exists": { "field": { "name.last" }}}
]
}
}
1.7.缓存
核心是一个字节集来表示哪些文档符合这个过滤器。
缓存的字节集是量更新的。
你索引中添加了新的文档,只有这些新文档需要被添加到已存的字节集中,而不是一遍遍重新计算整个缓存的过滤器。过滤器和整个系统的其他部分一样是实时的,你不需要关心缓存的过期时间。
独立的过滤缓存
每个过滤器都被独立计算和缓存,而不管它们在哪里使用。如果两个不同的查询使用相同的过滤器,则会使用相同的字节集。同样,如果一个查询在多处使用同样的过滤器,只有一个字节集会被计算和重用。
【举例】
"bool": {
"should": [
{ "bool": {
"must": [
{ "term": { "folder": "inbox" }}, // 这两个过滤器相同,而且会使用同一个字节集
{ "term": { "read": false }}
]
}},
{ "bool": {
"must_not": {
"term": { "folder": "inbox" } <1>
},
"must": {
"term": { "important": true }
}
}}
]
}
【解释】
虽然一个条件是 must 而另一个是 must_not ,这两个条件本身是相等的。这意味着字节集会在第一个条件执行时计算一次,然后作为缓存被另一个条件使用。而第二次执行这条查询时,过滤条件已经被缓存了,所以两个条件都能使用缓存的字节集。
控制缓存
大部分直接处理字段的枝叶过滤器(例如 term )会被缓存,而像 bool 这类的组合过滤器则不会被缓存。
【提示】
枝叶过滤器需要在硬盘中检索倒排索引,所以缓存它们是有意义的。
另一方面来说,组合过滤器使用快捷的字节逻辑来组合它们内部条件生成的字节集结果,所以每次重新计算它们也是很高效的。
有部分枝叶过滤器,默认不会被缓存,因为它们这样做没有意义:
① 脚本过滤器:
脚本过滤器的结果不能被缓存因为脚本的意义对于 Elasticsearch 来说是不透明的。
② Geo 过滤器:
定位过滤器,通常被用于过滤基于特定用户地理位置的结果。因为每个用户都有一个唯一的定位,geo 过滤器看起来不太会重用,所以缓存它们没有意义。
③ 日期范围:
使用 now 方法的日期范围,结果值精确到毫秒。每次这个过滤器执行时,now 返回一个新的值。老的过滤器将不再被使用,所以默认缓存是被禁用的。然而,当 now 被取整时,缓存默认是被启用的。
【测试】
{
"range" : {
"timestamp" : {
"gt" : "2014-01-02 16:15:14" <1>
},
"_cache": false // 禁用缓存,有时候默认的缓存测试并不正确
}
}
1.8.过滤顺序
在 bool 条件中过滤器的顺序对性能有很大的影响。更详细的过滤条件应该被放置在其他过滤器之前,以便在更早的排除更多的文档。
假如条件 A 匹配 1000 万个文档,而 B 只匹配 100 个文档,那么需要将 B 放在 A 前面。
缓存的过滤器非常快,所以它们需要被放在不能缓存的过滤器之前。
【举例】
GET /logs/2014-01/_search
{
"query" : {
"filtered" : {
"filter" : {
"range" : {
"timestamp" : {
"gt" : "now-1h"
}
}
}
}
}
}
【解释】
这个过滤条件没有被缓存,因为它使用了 now 方法,这个值每毫秒都在变化。每次执行这条查询时都检测一整个月的日志事件。
【优化】"bool": {
"must": [
{ "range" : {
"timestamp" : {
"gt" : "now-1h/d" // 被缓存,取整
}
}},
{ "range" : {
"timestamp" : {
"gt" : "now-1h" // 没有被缓存
}
}}
]
}