提高性能
短语和邻近度查询比简单的match查询在性能上更昂贵。match查询仅仅是查看词条是否存在于倒排索引(Inverted Index)中,而match_phrase查询则须要计算和比較多个可能反复词条(Multiple possibly repeated)的位置。
在Lucene Nightly Benchmarks中,显示了一个简单的term查询比一个短语查询快大概10倍,比一个邻近度查询(一个拥有slop的短语查询)快大概20倍。
当然,这个代价是在搜索期间而不是索引期间付出的。
TIP
通常,短语查询的额外代价并不像这些数字说的那么吓人。
实际上,性能上的差异仅仅是说明了一个简单的term查询时多么的快。在标准全文数据上进行的短语查询通常可以在数毫秒内完毕,因此它们在实际生产环境下是全然可以使用的,即使在一个繁忙的集群中。
在某些特定的场景下。短语查询可能会非常耗费资源。可是这样的情况时不常有的。
一个典型的样例是DNA序列,此时会在非常多位置上出现非常之多的同样反复词条。使用高slop值会使位置计算发生大幅度的增长。
因此,怎样可以限制短语和邻近度查询的性能消耗呢?一个实用的方法是降低须要使用短语查询进行检查的文档总数。
结果的分值重计算(Rescoring Results)
在上一节中,我们讨论了使用邻近度查询来调整相关度,而不是使用它来将文档从结果列表中加入或者排除。
一个查询可能会匹配百万计的结果。可是我们的用户非常可能仅仅对前面几页结果有兴趣。
一个简单的match查询已经通过排序将含有全部搜索词条的文档放在结果列表的前面了。而我们仅仅想对这些前面的结果进行又一次排序来给予那些同一时候匹配了短语查询的文档额外的相关度。
search API通过分值重计算(Rescoring)来支持这一行为。在分值重计算阶段。你可以使用一个更加昂贵的分值计算算法 - 比方一个短语查询 - 来为每一个分片的前K个结果又一次计算其分值。紧接着这些结果就会按其新的分值又一次排序。
该请求例如以下所看到的:
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": {
"query": "quick brown fox",
"minimum_should_match": "30%"
}
}
},
"rescore": {
"window_size": 50,
"query": {
"rescore_query": {
"match_phrase": {
"title": {
"query": "quick brown fox",
"slop": 50
}
}
}
}
}
}
match查询用来决定哪些文档会被包括在终于的结果集合中。结果通过TF/IDF进行排序。 window_size是每一个分片上须要又一次计算分值的数量。
寻找关联的单词(Finding Associated Words)
虽然短语和邻近度查询非常管用,它们还是有一个缺点。
它们过于严格了:全部的在短语查询中的词条都必须出如今文档中。即使使用了slop。
通过slop获得的可以调整单词顺序的灵活性也是有代价的,由于你失去了单词之间的关联。虽然你可以识别文档中的sue。alligator和ate出如今一块,可是你不能推断是Sue ate还是alligator ate。
当单词结合在一起使用时,它们表达的意思比单独使用时要丰富。
"I’m not happy I’m working"和"I’m happy I’m not working"含有同样的单词,也拥有相近的邻近度,可是它们的意思大相径庭。
假设我们索引单词对,而不是索引独立的单词,那么我们就行保留很多其它关于单词使用的上下文信息。
对于句子"Sue ate the alligator",我们不仅索引每一个单词(或者Unigram)为一个词条:
["sue", "ate", "the", "alligator"]
我们同一时候会将每一个单词和它的邻近单词一起索引成一个词条:
["sue ate", "ate the", "the alligator"]
这些单词对(也叫做Bigram)就是所谓的Shingle。
TIP
Shingle不限于仅仅是单词对;你也能够索引三个单词(Word Triplet,也被称为Trigram)作为一个词条:
["sue ate the", "ate the alligator"]
Trigram可以给你更高的精度,可是也大大地添加了索引的不同词条的数量。在多数情况下,Bigram就足够了。
当然。仅仅有当用户输入查询的顺序和原始文档的顺序一致,Shingle才可以起作用。一个针对sue alligator的查询会匹配单独的单词,可是不会匹配不论什么Shingle。
幸运的是,用户会倾向于使用和他们正在搜索的数据中相似的结构来表达查询。可是这是非常重要的一点:仅使用Bigram是不够的。我们仍然须要Unigram。我们能够将匹配Bigram作为信号(Signal)来添加相关度分值。
产生Shingle
Shingle须要在索引期间,作为分析过程的一部分被创建。我们可以将Unigram和Bigram都索引到一个字段中,可是将它们放在不同的字段中会更加清晰,也可以让它们可以被独立地查询。Unigram字段形成了我们搜索的基础部分,而Bigram字段则用来提升相关度。
首先。我们须要使用shingle词条过滤器来创建解析器:
DELETE /my_index
PUT /my_index
{
"settings": {
"number_of_shards": 1,
"analysis": {
"filter": {
"my_shingle_filter": {
"type": "shingle",
"min_shingle_size": 2,
"max_shingle_size": 2,
"output_unigrams": false
}
},
"analyzer": {
"my_shingle_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"my_shingle_filter"
]
}
}
}
}
}
默认Shingle的min/max值就是2。因此我们也能够不显式地指定它们。 output_unigrams被设置为false。用来避免将Unigram和Bigram索引到同样字段中。
让我们使用analyze API来測试该解析器:
GET /my_index/_analyze?analyzer=my_shingle_analyzer
Sue ate the alligator
不出所料,我们得到了3个词条:
- sue ate
- ate the
- the alligator
如今我们就能够创建一个使用新解析器的字段了。
多字段(Multifields)
将Unigram和Bigram分开索引会更加清晰,因此我们将title字段创建成一个多字段(Multifield)(參见字符串排序和多字段(String Sorting and Multifields)):
PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"title": {
"type": "string",
"fields": {
"shingles": {
"type": "string",
"analyzer": "my_shingle_analyzer"
}
}
}
}
}
}
有了上述映射,JSON文档中的title字段会以Unigram(title字段)和Bigram(title.shingles字段)的方式索引,从而让我们能够独立地对这两个字段进行查询。
最后,我们能够索引演示样例文档:
POST /my_index/my_type/_bulk
{ "index": { "_id": 1 }}
{ "title": "Sue ate the alligator" }
{ "index": { "_id": 2 }}
{ "title": "The alligator ate Sue" }
{ "index": { "_id": 3 }}
{ "title": "Sue never goes anywhere without her alligator skin purse" }
搜索Shingles
为了理解加入的shingles字段的优点,让我们首先看看一个针对"The hungry alligator ate Sue"的简单match查询的返回结果:
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": "the hungry alligator ate sue"
}
}
}
该查询会返回全部的3份文档。可是注意文档1和文档2拥有同样的相关度分值,由于它们含有同样的单词:
{
"hits": [
{
"_id": "1",
"_score": 0.44273707,
"_source": {
"title": "Sue ate the alligator"
}
},
{
"_id": "2",
"_score": 0.44273707,
"_source": {
"title": "The alligator ate Sue"
}
},
{
"_id": "3",
"_score": 0.046571054,
"_source": {
"title": "Sue never goes anywhere without her alligator skin purse"
}
}
]
}
如今让我们将shingles字段也加入到查询中。记住我们会将shingle字段作为信号 - 以添加相关度分值 - 我们仍然须要将基本的title字段包括到查询中:
GET /my_index/my_type/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": "the hungry alligator ate sue"
}
},
"should": {
"match": {
"title.shingles": "the hungry alligator ate sue"
}
}
}
}
}
我们仍然匹配了3分文档。可是文档2如今排在了第一位,由于它匹配了Shingle词条"ate sue":
{
"hits": [
{
"_id": "2",
"_score": 0.4883322,
"_source": {
"title": "The alligator ate Sue"
}
},
{
"_id": "1",
"_score": 0.13422975,
"_source": {
"title": "Sue ate the alligator"
}
},
{
"_id": "3",
"_score": 0.014119488,
"_source": {
"title": "Sue never goes anywhere without her alligator skin purse"
}
}
]
}
即使在查询中包括了没有在不论什么文档中出现的单词hungry,我们仍然通过使用单词邻近度得到了最相关的文档。
性能
Shingle不仅比短语查询更灵活,它们的性能也更好。相比每次搜索须要为短语查询付出的代价,对Shingle的查询和简单match查询一样的高效。仅仅是在索引期间会付出一点小代价,由于很多其它的词条须要被索引,意味着使用了Shingle的字段也会占用很多其它的磁盘空间。可是,多数应用是写入一次读取多次的,因此在索引期间花费一点代价来让查询更迅速是有意义的。
这是一个你在ES中常常会碰到的主题:让你在搜索期间可以做非常多事情,而不须要不论什么预先的设置。
一旦更好地了解了你的需求,就行通过在索引期间正确地建模来得到更好的结果和更好的性能。