全文检索
我们已经介绍了简单的结构化查询,下面开始介绍全文检索:怎样对全文字段(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" }
}
}
}
}
匹配查询
不管你搜索什么内容,match
查询是你首先需要接触的查询。它是一个高级查询,意味着match
查询知道如何更好的处理全文检索和准确值检索。
这也就是说,match
查询的一个主要用途是进行全文搜索。让我们通过一个小例子来看一下全文搜索是如何工作的。
索引一些数据
首先,我们使用bulk
API来创建和索引一些文档:
DELETE /my_index <1>
PUT /my_index
{ "settings": { "number_of_shards": 1 }} <2>
POST /my_index/my_type/_bulk
{ "index": { "_id": 1 }}
{ "title": "The quick brown fox" }
{ "index": { "_id": 2 }}
{ "title": "The quick brown fox jumps over the lazy dog" }
{ "index": { "_id": 3 }}
{ "title": "The quick brown fox jumps over the quick dog" }
{ "index": { "_id": 4 }}
{ "title": "Brown fox brown dog" }
// SENSE: 100_Full_Text_Search/05_Match_query.json
<1> 删除已经存在的索引(如果索引存在)
<2> 然后,关联失效
这一节解释了为什么我们创建该索引的时候只使用一个主分片。
单词查询
第一个例子解释了当使用match
查询进行单词全文搜索时发生了什么:
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": "QUICK!"
}
}
}
// SENSE: 100_Full_Text_Search/05_Match_query.json
Elasticsearch通过下面的步骤执行match
查询:
检查field类型
title
字段是一个字符串(analyzed
),所以该查询字符串也需要被分析(analyzed
)分析查询字符串
查询词QUICK!
经过标准分析器的分析后变成单词quick
。因为我们只有一个查询词,因此match
查询可以以一种低级别term
查询的方式执行。找到匹配的文档
term
查询在倒排索引中搜索quick
,并且返回包含该词的文档。在这个例子中,返回的文档是1,2,3。为每个文档打分
term
查询综合考虑词频(每篇文档title
字段包含quick
的次数)、逆文档频率(在全部文档中title
字段包含quick
的次数)、包含quick
的字段长度(长度越短越相关)来计算每篇文档的相关性得分_score
。(更多请见相关性介绍)
这个过程之后我们将得到以下结果(简化后):
"hits": [
{
"_id": "1",
"_score": 0.5, <1>
"_source": {
"title": "The quick brown fox"
}
},
{
"_id": "3",
"_score": 0.44194174, <2>
"_source": {
"title": "The quick brown fox jumps over the quick dog"
}
},
{
"_id": "2",
"_score": 0.3125, <2>
"_source": {
"title": "The quick brown fox jumps over the lazy dog"
}
}
]
<1> 文档1最相关,因为 title
最短,意味着quick
在语义中起比较大的作用。
<2> 文档3比文档2更相关,因为在文档3中quick
出现了两次。
多词查询
如果一次只能查询一个关键词,全文检索将会很不方便。幸运的是,用match
查询进行多词查询也很简单:
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": "BROWN DOG!"
}
}
}
上面这个查询返回以下结果集:
{
"hits": [
{
"_id": "4",
"_score": 0.73185337, <1>
"_source": {
"title": "Brown fox brown dog"
}
},
{
"_id": "2",
"_score": 0.47486103, <2>
"_source": {
"title": "The quick brown fox jumps over the lazy dog"
}
},
{
"_id": "3",
"_score": 0.47486103, <2>
"_source": {
"title": "The quick brown fox jumps over the quick dog"
}
},
{
"_id": "1",
"_score": 0.11914785, <3>
"_source": {
"title": "The quick brown fox"
}
}
]
}
<1> 文档4的相关度最高,因为包含两个"brown"和一个"dog"。
<2> 文档2和3都包含一个"brown"和一个"dog",且'title'字段长度相同,所以相关度相等。
<3> 文档1只包含一个"brown",不包含"dog",所以相关度最低。
因为match
查询需要查询两个关键词:"brown"
和"dog"
,在内部会执行两个term
查询并综合二者的结果得到最终的结果。match
的实现方式是将两个term
查询放入一个bool
查询,bool
查询在之前的章节已经介绍过。
重要的一点是,'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"
}
}
}
}
<1> 为了加入``'operator'``参数,``match``查询的结构有一些不同。
这个查询会排除文档1,因为文档1只包含了一个查询关键词。
控制精度
在 all 和 any 之间的选择有点过于非黑即白。如果用户指定了5个查询关键字,而一个文档只包含了其中的4个?将'operator