目录
1.简介
怎样对全文字段(full-text fields) 进行检索以找到相关度最高的文档。
全文检索最重要的两个方面是:
① 相关度(Relevance)
根据文档与查询的相关程度对结果集进行排序的能力。相关度可以使用TF/IDF、地理位置相近程度、模糊相似度或其他算法计算。
② 分析(Analysis)
将一段文本转换为一组唯一的、标准化了的标记(token),用以(a)创建倒排索引,(b)查询倒排索引。
注意,一旦提到相关度和分析,指的都是查询(queries)而非过滤器(filters)。
基于短语 vs. 全文
虽然所有的查询都会进行相关度计算,但不是所有的查询都有分析阶段。
而且像 bool 或 function_score 这样的查询并不在文本字段执行。
文本查询可以分为两大类:
1. 基于短语(Term-based)的查询:
像 term 或 fuzzy 一类的查询是低级查询,它们没有分析阶段。这些查询在单一的短语上执行。例如对单词 'Foo' 的 term 查询会在倒排索引里精确地查找 'Foo' 这个词,并对每个包含这个单词的文档计算TF/IDF相关度 '_score' 。
牢记 term 查询只在倒排查询里精确地查找特定短语,而不会匹配短语的其它变形,如 foo 或 FOO 。不管短语怎样被加入索引,都只匹配倒排索引里的准确值。如果你在一个设置了 'not_analyzed' 的字段为 '["Foo", "Bar"]' 建索引,或者在一个用 'whitespace' 解析器解析的字段为 'Foo Bar' 建索引,都会在倒排索引里加入两个索引 'Foo' 和 'Bar' 。
2.全文(Full-text)检索
match 和 query_string 这样的查询是高级查询,它们会对字段进行分析:
- 如果检索一个 'date' 或 'integer' 字段,它们会把查询语句作为日期或者整数格式数据。
- 如果检索一个准确值( 'not_analyzed' )字符串字段,它们会把整个查询语句作为一个短语。
- 如果检索一个全文( 'analyzed' )字段,查询会先用适当的解析器解析查询语句,产生需要查询的短语列表。然后对列表中的每个短语执行低级查询,合并查询结果,得到最终的文档相关度。
【提示】
如果确实要查询一个准确值字段( 'not_analyzed' ),需要考虑使用查询还是过滤器?
单一短语的查询通常相当于是/否问题,用过滤器可以更好的描述这类查询,并且过滤器缓存可以提升性能:
GET /_search
{
"query": {
"filtered": {
"filter": {
"term": { "gender": "female" }
}
}
}
}
1.1.匹配查询
match 查询,一个高级查询,一个主要用途是进行全文搜索
单词查询
【举例】
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": "QUICK!"
}
}
}
【步骤】
1. 检查field类型
title 字段是一个字符串(analyzed),所以该查询字符串也需要被分析(analyzed)
2. 分析查询字符串
查询词QUICK! 经过标准分析器的分析后变成单词 quick。因为只有一个查询词,因此match查询可以以一种低级别term查询的方式执行。
3. 找到匹配的文档
term 查询在倒排索引中搜索 quick,并且返回包含该词的文档。
4. 为每个文档打分
term 查询综合考虑词频(每篇文档 title 字段包含 quick 的次数)、逆文档频率(在全部文档中 title 字段包含 quick 的次数)、包含 quick 的字段长度(长度越短越相关)来计算每篇文档的相关性得分 _score 。
【结果】
"hits": [
{
"_id": "1",
"_score": 0.5, //
"_source": {
"title": "The quick brown fox"
}
},
{
"_id": "3",
"_score": 0.44194174, //
"_source": {
"title": "The quick brown fox jumps over the quick dog"
}
},
{
"_id": "2",
"_score": 0.3125, //
"_source": {
"title": "The quick brown fox jumps over the lazy dog"
}
}
]
1.2.多词查询
【举例】
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": "BROWN DOG!"
}
}
}
【结果】
{
"hits": [
{
"_id": "4",
"_score": 0.73185337,
"_source": {
"title": "Brown fox brown dog"
}
},
{
"_id": "2",
"_score": 0.47486103,
"_source": {
"title": "The quick brown fox jumps over the lazy dog"
}
},
{
"_id": "3",
"_score": 0.47486103,
"_source": {
"title": "The quick brown fox jumps over the quick dog"
}
},
{
"_id": "1",
"_score": 0.11914785,
"_source": {
"title": "The quick brown fox"
}
}
]
}
重要的一点是, 'title' 字段包含至少一个查询关键字的文档都被认为是符合查询条件的。匹配的单词数越多,文档的相关度越高。
提高精度
匹配包含任意个数查询关键字的文档可能会得到一些看似不相关的结果,这是一种霰弹策略(shotgun approach)。
然而可能想得到包含所有查询关键字的文档。
换句话说,想得到的是匹配 'brown AND dog' 的文档,而非 'brown OR dog' 。
match 查询接受一个 'operator' 参数,默认值为 or 。如果要求所有查询关键字都匹配,可以更改参数值为 and
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": { <1>
"query": "BROWN DOG!",
"operator": "and"
}
}
}
}
控制精度
match 查询有 'minimum_should_match' 参数,参数值表示被视为相关的文档必须匹配的关键词个数。
参数值可以设为整数,也可以设置为百分数。
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": {
"query": "quick brown dog",
"minimum_should_match": "75%"
}
}
}
}
【提示】
'minimum_should_match' 参数很灵活,根据用户输入的关键词个数,可以采用不同的匹配规则。
1.3.组合查询
过滤器会做一个判断:
是否应该将文档添加到结果集? 查询会做更精细的判断.
不仅决定一个文档是否要添加到结果集,而且还要计算文档的相关性(relevant).
【举例】
GET /my_index/my_type/_search
{
"query": {
"bool": {
"must": { "match": { "title": "quick" }},
"must_not": { "match": { "title": "lazy" }},
"should": [
{ "match": { "title": "brown" }},
{ "match": { "title": "dog" }}
]
}
}
}
【结果】
{
"hits": [
{
"_id": "3",
"_score": 0.70134366,
"_source": {
"title": "The quick brown fox jumps over the quick dog"
}
},
{
"_id": "1",
"_score": 0.3312608,
"_source": {
"title": "The quick brown fox"
}
}
]
}
【得分计算】
布尔查询通过把所有符合 must 和 should 的子句得分加起来,然后除以 must 和 should 子句的总数为每个文档计算相关性得分。
must_not 子句并不影响得分;他们存在的意义是排除已经被包含的文档。
精度控制
所有的 must 子句必须匹配, 并且所有的 must_not 子句必须不匹配, 但是多少 should 子句应该匹配呢?
默认的,不需要匹配任何 should 子句,一种情况例外:如果没有 must 子句,就必须至少匹配一个 should 子句。
可以通过 minimum_should_match 参数控制多少 should 子句需要被匹配,这个参数可以是正整数,也可以是百分比。
【举例】
GET /my_index/my_type/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "brown" }},
{ "match": { "title": "fox" }},
{ "match": { "title": "dog" }}
],
"minimum_should_match": 2 // 至少匹配2个;也可以用百分比表示
}
}
}
1.4.布尔查询
{
"match": { "title": "brown fox"}
}
等价
{
"bool": {
"should": [
{ "term": { "title": "brown" }},
{ "term": { "title": "fox" }}
]
}
}
{
"match": {
"title": {
"query": "brown fox",
"operator": "and"
}
}
}
等价
{
"bool": {
"must": [
{ "term": { "title": "brown" }},
{ "term": { "title": "fox" }}
]
}
}
1.5.提高查询得分
可以在任何查询子句中指定一个 boost 值来控制相对权重,默认值为1。一个大于1 的 boost 值可以提高查询子句的相对权重。
GET /_search
{
"query": {
"bool": {
"must": {
"match": {
"content": { // content 字段必须包含 full , text , search 这三个单词
"query": "full text search",
"operator": "and"
}
}
},
"should": [ // 如果 content 字段也包含了“Elasticsearch”或者“Lucene”,则文档会有一个更高的得分
{ "match": { "content": "Elasticsearch" }},
{ "match": { "content": "Lucene" }}
]
}
}
}
【如果想给包含“Lucene”一词的文档比较高的得分,甚至给包含“Elasticsearch”一词更高的得分要怎么做呢?】
GET /_search
{
"query": {
"bool": {
"must": {
"match": { // 查询子句的 boost 值为默认值 1
"content": {
"query": "full text search",
"operator": "and"
}
}
},
"should": [
{ "match": {
"content": {
"query": "Elasticsearch",
"boost": 3 // 最高的 boost 值
}
}},
{ "match": {
"content": {
"query": "Lucene",
"boost": 2
}
}}
]
}
}
}
注意:
1. boost 参数用于提高子句的相对权重(boost 值大于 1)或者降低子句的相对权重(boost值在0-1之间),但是提高和降低并非是线性的。
2. boost 值被使用了新的得分是标准的。每个查询类型都会有一个独有的标准算法。一个更大的 boost 值可以得到一个更高的得分。
3. 如果自己实现了没有基于TF/IDF的得分模型,但是想得到更多的对于提高得分过程的控制,
可以使用 function_score 查询来调整一个文档的 boost 值而不用通过标准的步骤。
1.6.分析控制
查询只能查找在倒排索引中出现的词,所以确保在文档索引的时候以及字符串查询的时候使用同一个分析器是很重要的,为了查询的词能够在倒排索引中匹配到。
字段配置一个指定的分析器或者直接使用类型,索引,或节点上的默认分析器。
【举例】
添加一个字段到 my_index
PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"english_title": {
"type": "string",
"analyzer": "english"
}
}
}
}
GET /my_index/_analyze?field=my_type.title // 使用 standard 分析器的 title 字段将会返回词 foxes
GET /my_index/_analyze?field=my_type.english_title // 使用 english 分析器的 english_title 字段将会返回词 fox
GET /my_index/my_type/_validate/query?explain
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Foxes"}},
{ "match": { "english_title": "Foxes"}}
]
}
}
}
【结果】
(title:foxes english_title:fox)
match 查询为每个字段使用合适的分析器用来确保在那个字段上可以用正确的格式查询这个词。
默认分析器
在创建索引的时候,Elasticsearch查找分析器的顺序如下:
- 在映射文件中指定字段的 analyzer ,或者
- 在文档的 _analyzer 字段上指定分析器,或者
- 在映射文件中指定类型的默认分析器 analyzer
- 在索引映射文件中设置默认分析器 default
- 在节点级别设置默认分析器 default
- standard 分析器
在查找索引的时候,Elasticsearch查找分析器的顺序如下:
- 在查询参数中指定 analyzer ,或者
- 在映射文件中指定字段的 analyzer ,或者
- 在映射文件中指定类型的默认分析器 analyzer
- 在索引映射文件中设置默认分析器 default
- 在节点级别设置默认分析器 default
- standard 分析器
【说明】
_analyzer 字段允许为每个文档指定默认的分析器
虽然在查询的时候指定 analyzer 参数,但是在一个索引中处理多种语言这并不是一个好方法,因为在多语言环境下很多问题会暴露出来。
有时候,在创建索引与查询索引的时候使用不同的分析器也是有意义的。
在创建索引的时候想要索引同义词
在查询索引的时候,我们不需要查询所有的同义词
为了满足这种差异,Elasticsearch支持 index_analyzer 和 search_analyzer 参数,
并且分析器被命名为 default_index 和 default_search。
把这些额外的参数考虑进去,Elasticsearch查找所有的分析器的顺序:
- 在映射文件中指定字段的 index_analyzer ,或者
- 在映射文件中指定字段的 analyzer ,或者
- 在文档的 _analyzer 字段上指定分析器,或者
- 在映射文件中指定类型的创建索引的默认分析器 index_analyzer
- 在映射文件中指定类型的默认分析器 analyzer
- 在索引映射文件中设置创建索引的默认分析器 default_index
- 在索引映射文件中设置默认分析器 default
- 在节点级别设置创建索引的默认分析器 default_index
- 在节点级别设置默认分析器 default
- standard 分析器
查询索引分析器的顺序:
- 在查询参数中指定 analyzer ,或者
- 在映射文件中指定字段的 search_analyzer ,或者
- 在映射文件中指定字段的 analyzer ,或者
- 在映射文件中指定类型的查询索引的默认分析器 analyzer
- 在索引映射文件中设置查询索引的默认分析器 default_search
- 在索引映射文件中设置默认分析器 default_search
- 在节点级别设置查询索引的默认分析器 default_search
- 在节点级别设置默认分析器 default
- standard 分析器
实际配置分析器
用索引配置,而不是用配置文件
【解释】
虽然开始使用Elasticsearch仅仅只是为了一个简单的目的或者是一个应用比如日志等等,但是结局是在同一个集群中运行着好几个不同的应用。每一个索引都需要是独立的,并且可以被独立的配置。不要想着给一个案例设置默认值,但是不得不重写他们来适配后面的案例。
这个规则把节点级别的配置分析器方法排除在外了。另外,节点级别的分析器配置方法需要改变每个节点的配置文件并且重启每个节点,这将成为维护的噩梦。保持Elasticsearch持续运行并通过API来管理索引的设置是一个更好的方法。
保持简便性
大多数时间,可以预先知道文档会包含哪些字段。最简单的方法是在你创建索引或者添加类型映射的时候为每一个全文检索字段设置分析器。虽然这个方法有点啰嗦,但是它可以很容易的看到哪个字段应用了哪个分析器。
通常情况下,大部分的字符串字段是确切值 not_analyzed 字段(索引但不分析字段)比如标签,枚举,加上少数全文检索字段会使用默认的分析器,像 standard 或者 english 或者其他语言。然后可能只有一到两个字段需要定制分析:或许 title 字段需要按照查找的方式去索引来支持查找。(指的是查找的字符串用什么分析器,创建索引就用什么分析器)
可以在索引设置 default 分析器的地方为几乎所有全文检索字段设置成想要的分析器,并且只要在一到两个字段指定专门的分析器。如果,在模型中,每个类型都需要不同的分析器,那么在类型级别使用 analyzer 配置来代替。
提示:
一个普通的像日志一样的基于时间轴的工作流数据每天都得创建新的索引,忙着不断的创建索引。
虽然这种工作流阻止预先创建索引,但是可以使用索引模板来指定新的索引的配置和映射。
1.7.关联失效
为什么我们只用一个主分片来创建索引?
【问题描述】
该用户创建了一些文档,执行了一个简单的查询,结果发现相关性较低的结果排在了相关性较高的结果的前面。
【解释】
假设用两个分片创建一个索引,以及索引10个文档,6个文档包含词 foo ,这样可能会出现分片1中有3个文档包含 foo ,分片2中也有三个文档包含 foo 。换句话说,我们的文档做了比较好的分布式。
默认的相似算法:词频率/反转文档频率(TF/IDF)
词频率是一个词在我们当前查询的文档的字段中出现的次数。出现的次数越多,相关性就越大。
反转文档频率指的是该索引中所有文档数与出现这个词的文件数的百分比,词出现的频率越大,IDF越小。
然而,由于性能问题,Elasticsearch不通过索引中所有的文档计算IDF。每个分片会为分片中所有的文档计算一个本地的IDF取而代之。
因为文档做了很好的分布式,每个分片的IDF是相同的。现在假设5个包含 foo 的文档在分片1中,以及其他6各文档在分片2中。在这个场景下,词 foo 在第一个分片中是非常普通的(因此重要性较小),但是在另一个分片中是很稀少的(因此重要性较高)。这些区别在IDF中就会产生不正确的结果。
事实证明,这并不是一个问题。索引越多的文档,本地IDF和全局IDF的区别就会越少。在实际工作的数据量下,本地IDF立刻能够很好的工作(With real-world volumes of data, thelocal IDFs soon even out,不知道这么翻译合不合适)。所以问题不是因为关联失效,而是
因为数据太少。
为了测试的目的,对于这个问题,有两种方法可以奏效。第一种方法是创建一个只有一个主分片的索引,像我们介绍 match 查询那节一样做。如果你只有一个分片,那么本地IDF就是全局IDF。
另一种方法是在请求中添加 ?search_type=dfs_query_then_fetch 。 dfs 就是Distributed Frequency Search,并且它会告诉Elasticsearch检查每一个分片的本地IDF为了计算整个索引的全局IDF。
提示:
不要把 dfs_query_then_fetch 用于生产环境。它实在是没有必要。
只要有足够的数据就能够确保词频率很好的分布。
没有理由在每个执行的查询中添加额外的DFS步骤。