elasticsearnch结构化搜索
结构化搜索
查询包含内部结构的数据,日期,时间,和数字都是结构化的;可以执行逻辑操作;
文本也可以格式化;
结构化搜索不关心文档的相关性或分数,它只是简单的包含或排除文档
一. 查找准确值
使用过滤器,非常的快,不计算相关性,很容易被缓存
用于数字的 term 过滤器
这个过滤器旨在处理数字,布尔值,日期,和文本。
一些产品最初用数字来索引,包含两个字段 price 和 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" }
找出特定价格的产品
在 Elasticsearch DSL 中,我们使用 term 过滤器,term 过滤器会查找我们设定的准确值;
它接受一个字段名和我们希望查找的值
{
    "term" : {
        "price" : 20
    }
}
term 过滤器本身并不能起作用,像在【查询 DSL】中介绍的一样,搜索 API 需要得到一个查询语句,而不是一个 过滤器。为了使用 term 过滤器,我们需要将它包含在一个过滤查询语句中:
GET /my_store/products/_search
{
    "query" : {
        "filtered" : { <1>
            "query" : {
                "match_all" : {} <2>
            },
            "filter" : {
                "term" : { <3>
                    "price" : 20
                }
            }
        }
    }
}
<1> filtered 查询同时接受接受 query 与 filter。
<2> match_all 用来匹配所有文档,这是默认行为,所以在以后的例子中我们将省略掉 query 部分。
<3> 这是我们上面见过的 term 过滤器。注意它在 filter 分句中的位置。
执行之后,你将得到预期的搜索结果:只能文档 2 被返回了
"hits" : [
    {
        "_index" : "my_store",
        "_type" :  "products",
        "_id" :    "2",
        "_score" : 1.0, <1>
        "_source" : {
          "price" :     20,
          "productID" : "KDKE-B-9947-#kL5"
        }
    }
]
过滤器不会执行计分和计算相关性。分值由 match_all 查询产生,所有文档一视同仁,所有每个结果的分值都是 1
用于文本的 term 过滤器
term 过滤器可以像匹配数字一样轻松的匹配字符串
转到查询 DSL,我们用 term 过滤器来构造一个类似的查询
GET /my_store/products/_search
{
    "query" : {
        "filtered" : {
            "filter" : {
                "term" : {
                    "productID" : "XHDK-A-1293-#fJ3"
                }
            }
        }
    }
}
这样不会有结果,productID会被分解成短语
设置这个字段为 not_analyzed 来告诉 Elasticsearch 它包含一个准确值
为了实现目标,我们要先删除旧索引(因为它包含了错误的映射),并创建一个正确映射的索引
DELETE /my_store <1>
PUT /my_store <2>
{
    "mappings" : {
        "products" : {
            "properties" : {
                "productID" : {
                    "type" : "string",
                    "index" : "not_analyzed" <3>
                }
            }
        }
    }
}
<1> 必须首先删除索引,因为我们不能修改已经存在的映射。
<2> 删除后,我们可以用自定义的映射来创建它。
<3> 这里我们明确表示不希望 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" }
内部过滤操作
Elasticsearch 在内部会通过一些操作来执行一次过滤:
查找匹配文档。
- term 过滤器在倒排索引中查找词 XHDK-A-1293-#fJ3,然后返回包含那个词的文档列表。在这个例子中,只有文档 1 有我们想要的词。
- 创建字节集
 然后过滤器将创建一个 字节集 —— 一个由 1 和 0 组成的数组 —— 描述哪些文档包含这个词。匹配的文档得到 1 字节,在我们的例子中,字节集将是 [1,0,0,0]
- 缓存字节集
 最后,字节集被储存在内存中,以使我们能用它来跳过步骤 1 和 2。这大大的提升了性能,让过滤变得非常的快。
当执行 filtered 查询时,filter 会比 query 早执行。结果字节集会被传给 query 来跳过已经被排除的文档。这种过滤器提升性能的方式,查询更少的文档意味着更快的速度。
二. 组合过滤
布尔过滤器
bool 过滤器由三部分组成:
{
   "bool" : {
      "must" :     [],
      "should" :   [],
      "must_not" : [],
   }
}
must:所有分句都必须匹配,与 AND 相同。
must_not:所有分句都必须不匹配,与 NOT 相同。
should:至少有一个分句匹配,与 OR 相同。
实现如下sql
SELECT product
FROM   products
WHERE  (price = 20 OR productID = "XHDK-A-1293-#fJ3")
  AND  (price != 30)
GET /my_store/products/_search
{
   "query" : {
      "filtered" : { <1>
         "filter" : {
            "bool" : {
              "should" : [
                 { "term" : {"price" : 20}}, <2>
                 { "term" : {"productID" : "XHDK-A-1293-#fJ3"}} <2>
              ],
              "must_not" : {
                 "term" : {"price" : 30} <3>
              }
           }
         }
      }
   }
}
<1> 注意我们仍然需要用 filtered 查询来包裹所有条件。+
<2> 这两个 term 过滤器是 bool 过滤器的子节点,因为它们被放在 should 分句下,所以至少他们要有一个条件符合。
<3> 如果一个产品价值 30,它就会被自动排除掉,因为它匹配了 must_not 分句。
我们的搜索结果返回了两个结果,分别满足了 bool 过滤器中的不同分句
"hits" : [
    {
        "_id" :     "1",
        "_score" :  1.0,
        "_source" : {
          "price" :     10,
          "productID" : "XHDK-A-1293-#fJ3" <1>
        }
    },
    {
        "_id" :     "2",
        "_score" :  1.0,
        "_source" : {
          "price" :     20, <2>
          "productID" : "KDKE-B-9947-#kL5"
        }
    }
]
<1> 匹配 term 过滤器 productID = "XHDK-A-1293-#fJ3"
<2> 匹配 term 过滤器 price = 20
嵌套布尔过滤器
实现如下sql
SELECT document
FROM   products
WHERE  productID      = "KDKE-B-9947-#kL5"
  OR (     productID = "JODL-X-1937-#pV7"
       AND price     = 30 )
GET /my_store/products/_search
{
   "query" : {
      "filtered" : {
         "filter" : {
            "bool" : {
              "should" : [
                { "term" : {"productID" : "KDKE-B-9947-#kL5"}}, <1>
                { "bool" : { <1>
                  "must" : [
                    { "term" : {"productID" : "JODL-X-1937-#pV7"}}, <2>
                    { "term" : {"price" : 30}} <2>
                  ]
                }}
              ]
           }
         }
      }
   }
}
<1> 因为 term 和 bool 在第一个 should 分句中是平级的,至少需要匹配其中的一个过滤器。+
<2> must 分句中有两个平级的 term 分句,所以他们俩都需要匹配。
结果得到两个文档,分别匹配一个 should 分句:
"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> productID 匹配第一个 bool 中的 term 过滤器。
<2> 这两个字段匹配嵌套的 bool 中的 term 过滤器。
这只是一个简单的例子,但是它展示了该怎样用布尔过滤器来构造复杂的逻辑条件。
三. 查询多个准确值
terms过滤器
{
    "terms" : {
        "price" : [20, 30]
    }
}
像 term 过滤器一样,我们将它放在 filtered 查询中:
GET /my_store/products/_search
{
    "query" : {
        "filtered" : {
            "filter" : {
                "terms" : { <1>
                    "price" : [20, 30]
                }
            }
        }
    }
}
<1> 这是前面提到的 terms 过滤器,放置在 filtered 查询中
这条查询将返回第二,第三和第四个文档:
"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"
        }
     }
]
四. 包含,而不是相等
包含,而不是相等
term 和 terms 是包含操作,而不是相等操作
{ "term" : { "tags" : "search" } }
匹配如下两个文档
{ "tags" : ["search"] }
{ "tags" : ["search", "open_source"] } <1>
term 过滤器是怎么工作的:检查所有具有该短语的文档,然后组成一个字节集,如下倒排索引
Token			DocIDs
open_source		2
search			1,2
term 和 terms 是 必须包含 操作,而不是 必须相等
完全匹配
完全匹配通过添加另一个字段来实现,这字段索引原字段包含值的个数
{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }
bool过滤器来限制短语个数
GET /my_index/my_type/_search
{
    "query": {
        "filtered" : {
            "filter" : {
                 "bool" : {
                    "must" : [
                        { "term" : { "tags" : "search" } }, <1>
                        { "term" : { "tag_count" : 1 } } <2>
                    ]
                }
            }
        }
    }
}
<1> 找出所有包含 search 短语的文档
<2> 但是确保文档只有一个标签
这将匹配只有一个 search 标签的文档,而不是匹配所有包含了 search 标签的文档。
五. 范围
数字范围
例如,你可能希望找到所有价格高于 20 元而低于 40 元的产品
在 SQL 语法中,范围可以如下表示:
SELECT document
FROM   products
WHERE  price BETWEEN 20 AND 40
Elasticsearch 有一个 range 过滤器,让你可以根据范围过滤:
"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" : {
    "price" : {
        "gt" : 20
    }
}
日期范围
range 过滤器也可以用于日期字段:
"range" : {
    "timestamp" : {
        "gt" : "2014-01-01 00:00:00",
        "lt" : "2014-01-07 00:00:00"
    }
}
当用于日期字段时,range 过滤器支持日期数学操作。例如,我们想找到所有最近一个小时的文档:
"range" : {
    "timestamp" : {
        "gt" : "now-1h"
    }
}
这个过滤器将始终能找出所有时间戳大于当前时间减 1 小时的文档,让这个过滤器像移窗一样通过你的文档。
日期计算也能用于实际的日期,而不是仅仅是一个像 now 一样的占位符。只要在日期后加上双竖线 ||,就能使用日期数学表达式了。
"range" : {
    "timestamp" : {
        "gt" : "2014-01-01 00:00:00",
        "lt" : "2014-01-01 00:00:00||+1M" <1>
    }
}
<1> 早于 2014 年 1 月 1 号加一个月
日期计算是与日历相关的,所以它知道每个月的天数,每年的天数,等等。
字符串范围
range 过滤器也可以用于字符串。字符串范围根据字典或字母顺序来计算。例如,这些值按照字典顺序排序:
5, 50, 6, B, C, a, ab, abb, abc, b
提示:倒排索引中的短语按照字典顺序排序,也是为什么字符串范围使用这个顺序。
假如我们想让范围从 a 开始而不包含 b,我们可以用类似的 range 过滤器语法:
"range" : {
    "title" : {
        "gte" : "a",
        "lt" :  "b"
    }
}
当心基数:
数字和日期字段的索引方式让他们在计算范围时十分高效。但对于字符串来说却不是这样。为了在字符串上执行范围操作,Elasticsearch 会在这个范围内的每个短语执行 term 操作。这比日期或数字的范围操作慢得多。
字符串范围适用于一个基数较小的字段,一个唯一短语个数较少的字段。你的唯一短语数越多,搜索就越慢。
六. 处理Null值
null,[](空数组)和 [null] 是相等的。它们都不存在于倒排索引中
然而数据经常会缺失字段,或包含空值或空数组。为了应对这些情形,Elasticsearch 有一些工具来处理空值或缺失的字段。
exists 过滤器
这个过滤器将返回任何包含这个字段的文档,让我们用标签来举例
POST /my_index/posts/_bulk
{ "index": { "_id": "1"              }}
{ "tags" : ["search"]                }  <1>
{ "index": { "_id": "2"              }}
{ "tags" : ["search", "open_source"] }  <2>
{ "index": { "_id": "3"              }}
{ "other_field" : "some data"        }  <3>
{ "index": { "_id": "4"              }}
{ "tags" : null                      }  <4>
{ "index": { "_id": "5"              }}
{ "tags" : ["search", null]          }  <5>
<1> tags 字段有一个值
<2> tags 字段有两个值
<3> tags 字段不存在
<4> tags 字段被设为 null
<5> tags 字段有一个值和一个 null
tags字段的倒排索引
Token			DocIDs
open_source		2
search			1,2,5
作用类似IS NOT 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] } <1>
    },
    {
      "_id" :     "2",
      "_score" :  1.0,
      "_source" : { "tags" : ["search", "open source"] }
    }
]
missing 过滤器
类似NULL
missing 过滤器来取代 exists
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_value 来处理明确的 null 值。没有值的字段仍将被排除在倒排索引外。
当选定一个合适的 null_value 时,确保以下几点:
- 它与字段的类型匹配,你不能在 date 类型的字段中使用字符串 null_value
- 它需要能与这个字段可能包含的正常值区分开来,以避免真实值和 null 值混淆
对象的 exists/missing
exists 和 missing 过滤器同样能在内联对象上工作,而不仅仅是核心类型。例如下面的文档:
{
   "name" : {
      "first" : "John",
      "last" :  "Smith"
   }
}
你可以检查 name.first 和 name.last 的存在性,也可以检查 name 的。然而,在【映射】中,我们提到对象在内部被转成扁平化的键值结构,像下面所示:
{
   "name.first" : "John",
   "name.last"  : "Smith"
}
所以我们是怎么使用 exists 或 missing 来检测 name 字段的呢,这个字段并没有真正存在于倒排索引中。
原因是像这样的一个过滤器
{
    "exists" : { "field" : "name" }
}
实际是这样执行的
{
    "bool": {
        "should": [
            { "exists": { "field": { "name.first" }}},
            { "exists": { "field": { "name.last"  }}}
        ]
    }
}
同样这意味着假如 first 和 last 都为空,那么 name 就是不存在的。
七. 缓存
过滤器的字节集可以被缓存
独立的过滤缓存
每个过滤器都被独立计算和缓存,而不管它们在哪里使用。
如果两个不同的查询使用相同的过滤器,则会使用相同的字节集。
查找符合下列条件的邮箱
- 在收件箱而且没有被读取过
- 不在收件箱但是被标记为重要
"bool": {
   "should": [
      { "bool": {
            "must": [
               { "term": { "folder": "inbox" }}, <1>
               { "term": { "read": false }}
            ]
      }},
      { "bool": {
            "must_not": {
               "term": { "folder": "inbox" } <1>
            },
            "must": {
               "term": { "important": true }
            }
      }}
   ]
}
<1> 这两个过滤器相同,而且会使用同一个字节集
虽然一个收件箱条件是 must 而另一个是 must_not,这两个条件本身是相等的。这意味着字节集会在第一个条件执行时计算一次,然后作为缓存被另一个条件使用。而第二次执行这条查询时,收件箱的过滤已经被缓存了,所以两个条件都能使用缓存的字节集。
控制缓存
大部分直接处理字段的枝叶过滤器(例如 term)会被缓存,而像 bool 这类的组合过滤器则不会被缓存。
枝叶过滤器需要在硬盘中检索倒排索引,所以缓存它们是有意义的。另一方面来说,组合过滤器使用快捷的字节逻辑来组合它们内部条件生成的字节集结果,所以每次重新计算它们也是很高效的。
有部分枝叶过滤器,默认不会被缓存,因为它们这样做没有意义:
脚本过滤器:
脚本过滤器的结果不能被缓存因为脚本的意义对于 Elasticsearch 来说是不透明的。
Geo 过滤器:
定位过滤器(我们会在【geoloc】中更详细的介绍),通常被用于过滤基于特定用户地理位置的结果。因为每个用户都有一个唯一的定位,geo 过滤器看起来不太会重用,所以缓存它们没有意义。
日期范围:
使用 now 方法的日期范围(例如 "now-1h"),结果值精确到毫秒。每次这个过滤器执行时,now 返回一个新的值。老的过滤器将不再被使用,所以默认缓存是被禁用的。然而,当 now 被取整时(例如,now/d 取最近一天),缓存默认是被启用的。
_cache可以设置过滤器缓存策略
{
    "range" : {
        "timestamp" : {
            "gt" : "2014-01-02 16:15:14" <1>
        },
        "_cache": false <2>
    }
}
<1> 看起来我们不会再使用这个精确时间戳
<2> 在这个过滤器上禁用缓存
八. 过滤顺序
在 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" <1>
            }
        }},
        { "range" : {
            "timestamp" : {
                "gt" : "now-1h" <2>
            }
        }}
    ]
}
<1> 这个过滤器被缓存了,因为它使用了取整到昨夜凌晨 now 条件。
<2> 这个过滤器没有被缓存,因为它没有对 now 取整。
now-1h/d 条件取整到昨夜凌晨,所以所有今天之前的文档都被排除掉了。这个结果的字节集被缓存了,因为 now 被取整了,意味着它只需要每天当昨夜凌晨的值改变时被执行一次。now-1h 条件没有被缓存,因为 now 表示最近一毫秒的时间。然而,得益于第一个过滤器,第二个过滤器只需要检测当天的文档就行。
这些条件的排序很重要。上面的实现能正常工作是因为自从昨晚凌晨条件比最近一小时条件位置更前。假如它们用别的方式组合,那么最近一小时条件还是需要检测所有的文档,而不仅仅是昨夜以来的文档。
 
                    
                     
                    
                 
                    
                
 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号