查询手段
一、单词查询(Single word query)
{
"query": {
"match": {
"title": "QUICK!"
}
}
}
- 检查字段类型
- 解析查询字符串
- 找到匹配的文档
- 对每份文档打分
二、多词查询(Multi-word Queries)
{
"query": {
"match": {
"title": "BROWN DOG!"
}
}
}
内部它需要执行两个term查询,然后将它们的结果合并来得到整体的结果,它会将两个term查询通过一个bool查询组织在一起
三、提高精度(Improving Precision)
{
"query": {
"match": {
"title": {
"query": "BROWN DOG!",
"operator": "and"
}
}
}
}
四、控制精度(Controlling Precision)
在all和any中选择有种非黑即白的感觉。如果用户指定了5个查询词条,而一份文档只包含了其中的4个呢?将"operator"设置成"and"会将它排除在外。
{
"query": {
"match": {
"title": {
"query": "quick brown dog",
"minimum_should_match": "75%"
}
}
}
}
五、合并查询(Combining Queries)
{
"query": {
"bool": {
"must": { "match": { "title": "quick" }},
"must_not": { "match": { "title": "lazy" }},
"should": [
{ "match": { "title": "brown" }},
{ "match": { "title": "dog" }}
]
}
}
}
通过must,must_not以及should参数来接受多个查询
title字段中含有词条quick,且不含有词条lazy的任何文档都会被作为结果返回。目前为止,它的工作方式和bool过滤器十分相似。
差别来自于两个should语句,它表达了这种意思:一份文档不被要求需要含有词条brown或者dog,但是如果它含有了,那么它的相关度应该更高。
六、分值计算(Score Calculation)
bool查询通过将匹配的must和should语句的_score相加,然后除以must和should语句的总数来得到相关度分值_score。
must_not语句不会影响分值;它们唯一的目的是将不需要的文档排除在外。
七、控制精度(Controlling Precision)
所有的must语句都需要匹配,而所有的must_not语句都不能匹配,但是should语句需要匹配多少个呢?默认情况下,should语句一个都不要求匹配,只有一个特例:如果查询中没有must语句,那么至少要匹配一个should语句。
{
"query": {
"bool": {
"should": [
{ "match": { "title": "brown" }},
{ "match": { "title": "fox" }},
{ "match": { "title": "dog" }}
],
"minimum_should_match": 2
}
}
}
minimum_should_match参数来控制should语句需要匹配的数量,该参数可以是一个绝对数值或者一个百分比
八、下面两个子句分别等价
{
"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" }}
]
}
}
-----------------------------------------------
{
"match": {
"title": {
"query": "quick brown fox",
"minimum_should_match": "75%"
}
}
}
{
"bool": {
"should": [
{ "term": { "title": "brown" }},
{ "term": { "title": "fox" }},
{ "term": { "title": "quick" }}
],
"minimum_should_match": 2
}
}
九、提升查询子句(Boosting Query Clause)
{
"query": {
"bool": {
"must": {
"match": {
"content": {
"query": "full text search",
"operator": "and"
}
}
},
"should": [
{ "match": { "content": "Elasticsearch" }},
{ "match": { "content": "Lucene" }}
]
}
}
}
指定一个boost值来控制每个查询子句的相对权重
{
"query": {
"bool": {
"must": {
"match": {
"content": {
"query": "full text search",
"operator": "and"
}
}
},
"should": [
{ "match": {
"content": {
"query": "Elasticsearch",
"boost": 3
}
}},
{ "match": {
"content": {
"query": "Lucene",
"boost": 2
}
}}
]
}
}
}
十、控制分析(Controlling Analysis)
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }}
]
}
}
}
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
bool查询中包含的译者查询子句只占了总分值的三分之一,如果我们将译者查询子句放到和标题及作者相同的层次上,就会减少标题和作者子句的权重,让它们各自只占四分之一。
{
"query": {
"bool": {
"should": [
{ "match": {
"title": {
"query": "War and Peace",
"boost": 2
}}},
{ "match": {
"author": {
"query": "Leo Tolstoy",
"boost": 2
}}},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
boost值的范围推荐在1和10之间
===============================================================
查询策略
策略:
- 最佳字段(Best fields)
- 多数字段(Most fields)
- 跨字段(Cross fields)
一、最佳字段(Best Fields)
bool should查询是如何计算得到其分值的
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
- 运行should子句中的两个查询
- 相加查询返回的分值
- 将相加得到的分值乘以匹配的查询子句的数量
- 除以总的查询子句的数量
dis_max查询
返回匹配了任何查询的文档,并且分值是产生了最佳匹配的查询所对应的分值
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
tie_breaker
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.3
}
}
}
tie_breaker参数会让dis_max查询的行为更像是dis_max和bool的一种折中。
dis_max最佳字段(Best fields)、bool多数字段(Most fields)的折中
它会通过下面的方式改变分值计算过程:
- 取得最佳匹配查询子句的_score。
- 将其它每个匹配的子句的分值乘以tie_breaker。
- 将以上得到的分值进行累加并规范化。
通过tie_breaker参数,所有匹配的子句都会起作用,只不过最佳匹配子句的作用更大。
二、multi_match、match、dis_max、bool 的转换
multi_match查询 下两个查询等价
{
"dis_max": {
"queries": [
{
"match": {
"title": {
"query": "Quick brown fox",
"minimum_should_match": "30%"
}
}
},
{
"match": {
"body": {
"query": "Quick brown fox",
"minimum_should_match": "30%"
}
}
},
],
"tie_breaker": 0.3
}
}
{
"multi_match": {
"query": "Quick brown fox",
"type": "best_fields",
"fields": [ "title", "body" ],
"tie_breaker": 0.3,
"minimum_should_match": "30%"
}
}
注意到以上的type属性为best_fields。 minimum_should_match和operator参数会被传入到生成的match查询中
在字段名中使用通配符
{
"multi_match": {
"query": "Quick brown fox",
"fields": "*_title"
}
}
提升个别字段
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "*_title", "chapter_title^2" ]
}
}
三、多字段映射(Multifield Mapping) 与 多数字段(Most fields)
{
"settings": { "number_of_shards": 1 },
"mappings": {
"my_type": {
"properties": {
"title": {
"type": "text",
"analyzer": "english",
"fields": {
"std": {
"type": "text",
"analyzer": "standard"
}
}
}
}
}
}
}
title字段使用了english解析器进行词干提取。 title.std字段则使用的是standard解析器,因此它没有进行词干提取。
PUT /my_index/my_type/1
{ "title": "My rabbit jumps" }
PUT /my_index/my_type/2
{ "title": "Jumping jack rabbits" }
{
"query": {
"match": {
"title": "jumping rabbits"
}
}
}
它会变成一个针对两个提干后的词条jump和rabbit的查询,这要得益于english解析器。两份文档的title字段都包含了以上两个词条,因此两份文档的分值是相同的
如果我们只查询title.std字段,那么只有文档2会匹配。但是,当我们查询两个字段并将它们的分值通过bool查询进行合并的话,两份文档都能够匹配(title字段也匹配了),而文档2的分值会更高一些(匹配了title.std字段):
{
"query": {
"multi_match": {
"query": "jumping rabbits",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}
在上述查询中,由于我们想合并所有匹配字段的分值,因此使用的类型为most_fields。这会让multi_match查询将针对两个字段的查询子句包含在一个bool查询中,而不是包含在一个dis_max查询中。
我们使用了拥有宽泛形式的title字段来匹配尽可能多的文档 - 来增加召回率(Recall),同时也使用了title.std字段作为信号来让最相关的文档能够拥有更靠前的排序
每个字段对最终分值的贡献可以通过指定boost值进行控制。比如,我们可以提升title字段来让该字段更加重要,这也减小了其它信号字段的影响
GET /my_index/_search
{
"query": {
"multi_match": {
"query": "jumping rabbits",
"type": "most_fields",
"fields": [ "title^10", "title.std" ]
}
}
}
使用most_fields方法执行实体查询有一些不那么明显的问题:
- 它被设计用来找到匹配任意单词的多数字段,而不是找到跨越所有字段的最匹配的单词。
- 它不能使用operator或者minimum_should_match参数来减少低相关度结果带来的长尾效应。
- 每个字段的词条频度是不同的,会互相干扰最终得到较差的排序结果。
四、自定义_all字段
PUT /my_index
{
"mappings": {
"person": {
"properties": {
"first_name": {
"type": "string",
"copy_to": "full_name"
},
"last_name": {
"type": "string",
"copy_to": "full_name"
},
"full_name": {
"type": "string"
}
}
}
}
}
有了这个映射,我们可以通过first_name字段查询名字,last_name字段查询姓氏,或者full_name字段查询姓氏和名字。
五、跨域查询(Cross-fields Queries)
如果你在索引文档前就能够自定义_all字段的话,那么使用_all字段就是一个不错的方法。但是,ES同时也提供了一个搜索期间的解决方案:使用类型为cross_fields的multi_match查询。
cross_fields类型采用了一种以词条为中心(Term-centric)的方法,这种方法和best_fields及most_fields采用的以字段为中心(Field-centric)的方法有很大的区别,它将所有的字段视为一个大的字段,然后在任一字段中搜索每个词条。
为了阐述以字段为中心和以词条为中心的查询的区别,看看以字段为中心的most_fields查询的解释(译注:通过validate-query API得到):
GET /_validate/query?explain
best_fields 以字段为中心
best_fields 它会为每个字段生成一个match查询,然后将这些查询包含在一个dis_max查询中
单词条:operator : and or 查询结果相同
{
"query":{
"multi_match":{
"query":"Keen",
"fields":["address","firstname"],
"operator":"and",
"type":"best_fields"
}
}
}
+(address:keen | firstname:keen) |代表得分最高一个 空格代表匹配得分相加 +代表必须 -代表必须否定
{
"query":{
"multi_match":{
"query":"Keen Court",
"fields":["address","firstname"],
"operator":"and",
"type":"best_fields"
}
}
}
+((+address:keen +address:court) | (+firstname:keen +firstname:court))
{
"query":{
"multi_match":{
"query":"Keen Court",
"fields":["address","firstname"],
"operator":"or",
"type":"best_fields"
}
}
}
+((address:keen address:court) | (firstname:keen firstname:court))
most_fields 以字段为中心
most_fields 它会为每个字段生成一个match查询,然后将这些查询包含在一个bool查询中
{
"query":{
"multi_match":{
"query":"Keen Court",
"fields":["address","firstname"],
"operator":"and",
"type":"most_fields"
}
}
}
+((+address:keen +address:court) (+firstname:keen +firstname:court))
{
"query":{
"multi_match":{
"query":"Keen Court",
"fields":["address","firstname"],
"operator":"or",
"type":"most_fields"
}
}
}
+((address:keen address:court) (firstname:keen firstname:court))
cross_fields 以查询词条为中心
{
"query":{
"multi_match":{
"query":"Keen Court",
"fields":["address","firstname"],
"operator":"and",
"type":"cross_fields"
}
}
}
+(+blended(terms:[address:keen, firstname:keen]) +blended(terms:[address:court, firstname:court]))
{
"query":{
"multi_match":{
"query":"Keen Court",
"fields":["address","firstname"],
"operator":"or",
"type":"cross_fields"
}
}
}
+(blended(terms:[address:keen, firstname:keen]) blended(terms:[address:court, firstname:court]))
六、cross_fields逐字段提升(Per-field Boosting)
使用cross_fields查询相比使用自定义_all字段的一个优点是你能够在查询期间对个别字段进行提升。
对于first_name和last_name这类拥有近似值的字段,也许提升是不必要的,但是如果你通过title和description字段来搜索书籍,那么你或许会给予title字段更多的权重。这可以通过前面介绍的caret(^)语法来完成:
GET /books/_search
{
"query": {
"multi_match": {
"query": "peter smith",
"type": "cross_fields",
"fields": [ "title^2", "description" ]
}
}
}
能够对个别字段进行提升带来的优势应该和对多个字段执行查询伴随的代价进行权衡
临近匹配
一、短语匹配(Phrase Matching)
就像一提到全文搜索会首先想到match查询一样,当你需要寻找邻近的几个单词时,你会使用match_phrase查询:
GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": "quick brown fox"
}
}
}
位置信息可以被保存在倒排索引(Inverted Index)中,像match_phrase这样位置感知(Position-aware)的查询能够使用位置信息来匹配那些含有正确单词出现顺序的文档,在这些单词间没有插入别的单词。
短语是什么
对于匹配了短语"quick brown fox"的文档,下面的条件必须为true:
- quick,brown和fox必须全部出现在某个字段中。
- brown的位置必须比quick的位置大1。
- fox的位置必须比quick的位置大2。
如果以上的任何条件没有被满足,那么文档就不能被匹配。
二、混合起来(Mixing it up)
精确短语(Exact-phrase)匹配也许太过于严格了。也许我们希望含有"quick brown fox"的文档也能够匹配"quick fox"查询,即使位置并不是完全相等的。
我们可以在短语匹配使用slop参数来引入一些灵活性:
GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick fox",
"slop": 1
}
}
}
}
slop参数告诉match_phrase查询词条能够相隔多远时仍然将文档视为匹配。相隔多远的意思是,你需要移动一个词条多少次来让查询和文档匹配?
我们以一个简单的例子来阐述这个概念。为了让查询quick fox能够匹配含有quick brown fox的文档,我们需要slop的值为1:
Pos 1 Pos 2 Pos 3
-----------------------------------------------
Doc: quick brown fox
-----------------------------------------------
Query: quick fox
Slop 1: quick ↳ fox
尽管在使用了slop的短语匹配中,所有的单词都需要出现,但是单词的出现顺序可以不同。如果slop的值足够大,那么单词的顺序可以是任意的。
三、多值的单字段(Multivalue Fields)
在多值字段上使用短语匹配会产生古怪的行为:
PUT /my_index/groups/1
{
"names": [ "John Abraham", "Lincoln Smith"]
}
默认doc文件结构建模是这样的
PUT /my_index/_mapping/groups
{
"properties": {
"names": {
"type": "text",
"position_offset_gap": 100
}
}
}
position_offset_gap设置告诉ES需要为数组中的每个新元素设置一个偏差值。因此,当我们再索引以上的人名数组时,会产生如下的结果:
- 位置1:john
- 位置2:abraham
- 位置103:lincoln
- 位置104:smith
现在我们的短语匹配就无法匹配该文档了,因为abraham和lincoln之间的距离为100。你必须要添加一个值为100的slop的值才能匹配。
运行一个针对Abraham Lincoln的短语查询:
{
"query": {
"match_phrase": {
"names": {
"query":"Abraham Lincoln",
"slop":100
}
}
}
}
"hits": [
{
"_index": "my_index",
"_type": "groups",
"_id": "1",
"_score": 0.010358438,
"_source": {
"names": [
"John Abraham",
"Lincoln Smith"
]
}
}
]
四、越近越好(Closer is better)
短语查询(Phrase Query)只是简单地将不含有精确查询短语的文档排除在外,而邻近查询(Proximity Query) - 一个slop值大于0的短语查询 - 会将查询词条的邻近度也考虑到最终的相关度_score中。通过设置一个像50或100这样的高slop值,你可以排除那些单词过远的文档,但是也给予了那些单词邻近的文档一个更高的分值。
下面针对quick dog的邻近查询匹配了含有quick和dog的两份文档,但是给与了quick和dog更加邻近的文档一个更高的分值:
POST /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick dog",
"slop": 50
}
}
}
}
五、使用邻近度来提高相关度
尽管邻近度查询(Proximity Query)管用,但是所有的词条都必须出现在文档的这一要求显的过于严格了。这个问题和我们在全文搜索(Full-Text Search)一章的精度控制(Controlling Precision)一节中讨论过的类似:如果7个词条中有6个匹配了,那么该文档也许对于用户而言已经足够相关了,但是match_phrase查询会将它排除在外。
相比将邻近度匹配作为一个绝对的要求,我们可以将它当做一个信号(Signal) - 作为众多潜在匹配中的一员,会对每份文档的最终分值作出贡献(参考多数字段(Most Fields))。
我们需要将多个查询的分值累加这一事实表示我们应该使用bool查询将它们合并。
我们可以使用一个简单的match查询作为一个must子句。该查询用于决定哪些文档需要被包含到结果集中。可以通过minimum_should_match参数来去除长尾(Long tail)。然后我们以should子句的形式添加更多特定查询。每个匹配了should子句的文档都会增加其相关度。
GET /my_index/my_type/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": {
"query": "quick brown fox",
"minimum_should_match": "30%"
}
}
},
"should": {
"match_phrase": {
"title": {
"query": "quick brown fox",
"slop": 50
}
}
}
}
}
}
可以向should子句中添加其它的查询,每个查询都用来增加特定类型的相关度
六、结果的分值重计算(Rescoring Results)
在上一节中,我们讨论了使用邻近度查询来调整相关度,而不是使用它来将文档从结果列表中添加或者排除。一个查询可能会匹配百万计的结果,但是我们的用户很可能只对前面几页结果有兴趣。
一个简单的match查询已经通过排序将含有所有搜索词条的文档放在结果列表的前面了。而我们只想对这些前面的结果进行重新排序来给予那些同时匹配了短语查询的文档额外的相关度。
search API通过分值重计算(Rescoring)来支持这一行为。在分值重计算阶段,你能够使用一个更加昂贵的分值计算算法 - 比如一个短语查询 - 来为每个分片的前K个结果重新计算其分值。紧接着这些结果就会按其新的分值重新排序。
{
"query": {
"match": {
"address": {
"query": "Street Place",
"minimum_should_match": "50%"
}
}
},
"rescore": {
"window_size": 50,
"query": {
"rescore_query": {
"match_phrase": {
"address": {
"query": "Columbia Place",
"slop": 10
}
}
}
}
}
}
match查询用来决定哪些文档会被包含在最终的结果集合中,结果通过TF/IDF进行排序。 window_size是每个分片上需要重新计算分值的数量。
七、寻找关联的单词(Finding Associated Words)
尽管短语和邻近度查询很管用,它们还是有一个缺点。它们过于严格了:所有的在短语查询中的词条都必须出现在文档中,即使使用了slop。
通过slop获得的能够调整单词顺序的灵活性也是有代价的,因为你失去了单词之间的关联。尽管你能够识别文档中的sue,alligator和ate出现在一块,但是你不能判断是Sue ate还是alligator ate。
如果我们索引单词对,而不是索引独立的单词,那么我们就能够保留更多关于单词使用的上下文信息。
对于句子"Sue ate the alligator",我们不仅索引每个单词(或者Unigram)为一个词条:
["sue", "ate", "the", "alligator"]
我们同时会将每个单词和它的邻近单词一起索引成一个词条:
["sue ate", "ate the", "the alligator"]
这些单词对(也叫做Bigram)就是所谓的Shingle。
Shingle不限于只是单词对;你也可以索引三个单词(Word Triplet,也被称为Trigram)作为一个词条:
["sue ate the", "ate the alligator"]
Trigram能够给你更高的精度,但是也大大地增加了索引的不同词条的数量。在多数情况下,Bigram就足够了。
当然,只有当用户输入查询的顺序和原始文档的顺序一致,Shingle才能够起作用;一个针对sue alligator的查询会匹配单独的单词,但是不会匹配任何Shingle。
幸运的是,用户会倾向于使用和他们正在搜索的数据中相似的结构来表达查询。但是这是很重要的一点:仅使用Bigram(二元)是不够的;我们仍然需要Unigram(一元),我们可以将匹配Bigram作为信号(Signal)来增加相关度分值。
首先,我们需要使用shingle词条过滤器来创建解析器:
POST http://node1:9200/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 http://node1:9200/my_index/_analyze?analyzer=my_shingle_analyzer&text="Sue ate the alligator"
{
"tokens": [
{
"token": "sue ate",
"start_offset": 1,
"end_offset": 8,
"type": "shingle",
"position": 0
},
{
"token": "ate the",
"start_offset": 5,
"end_offset": 12,
"type": "shingle",
"position": 1
},
{
"token": "the alligator",
"start_offset": 9,
"end_offset": 22,
"type": "shingle",
"position": 2
}
]
}
现在我们就可以创建一个使用新解析器的字段了。
八、多字段(Multifields)
将Unigram和Bigram分开索引会更加清晰,因此我们将title字段创建成一个多字段(Multifield)(参见字符串排序和多字段(String Sorting and Multifields)):
PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"title": {
"type": "text",
"fields": {
"shingles": {
"type": "text",
"analyzer": "my_shingle_analyzer"
}
}
}
}
}
}
或者==========================================================
PUT http://node1:9200/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"
]
}
}
}
},
"mappings":{
"my_type": {
"properties": {
"title": {
"type": "text",
"fields": {
"shingles": {
"type": "text",
"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的字段也会占用更多的磁盘空间。但是,多数应用是写入一次读取多次的,因此在索引期间花费一点代价来让查询更迅速是有意义的。
部分匹配(Partial Matching)前缀查询
一、前缀查询
PUT /my_index
{
"mappings": {
"address": {
"properties": {
"postcode": {
"type": "text",
"index": "not_analyzed"
}
}
}
}
}
然后索引一些邮政编码:
PUT /my_index/address/1
{ "postcode": "W1V 3DG" }
PUT /my_index/address/2
{ "postcode": "W2F 8HW" }
PUT /my_index/address/3
{ "postcode": "W1F 7HW" }
PUT /my_index/address/4
{ "postcode": "WC1N 1LZ" }
PUT /my_index/address/5
{ "postcode": "SW5 0BE" }
一、前缀查询(Prefix Query)
我们可以通过一个简单的prefix查询来得到所有以W1开头的邮政编码:
GET /my_index/address/_search
{
"query": {
"prefix": {
"postcode": "w1"
}
}
}
在本章的稍后部分,我们会介绍一种让前缀匹配更具效率的索引期间解决方案。但是首先,让我们看看两个相关的查询:wildcard以及regexp查询。
二、通配符
wildcard查询和prefix查询类似,也是一个基于词条的低级别查询。但是它能够让你指定一个模式(Pattern),而不是一个前缀(Prefix)。它使用标准的shell通配符:?用来匹配任意字符,*用来匹配零个或者多个字符。
以下查询能够匹配包含W1F 7HW和W2F 8HW的文档:
GET /my_index/address/_search
{
"query": {
"wildcard": {
"postcode": "W?F*HW"
}
}
}
三、正则表达式
假设现在你想匹配在W地域(Area)的所有邮政编码。使用前缀匹配时,以WC开头的邮政编码也会被匹配,在使用通配符查询时也会遇到类似的问题。我们只想匹配以W开头,紧跟着数字的邮政编码。使用regexp查询能够让你写下更复杂的模式:
GET /my_index/address/_search
{
"query": {
"regexp": {
"postcode": "W[0-9].+"
}
}
}
这个正则表达式的规定了词条需要以W开头,紧跟着一个0到9的数字,然后是一个或者多个其它字符。
注意
prefix,wildcard以及regexp查询基于词条进行操作。如果你在一个analyzed字段上使用了它们,它们会检查字段中的每个词条,而不是整个字段。
四、查询期间的即时搜索(Query-time Search-as-you-type)
{
"match_phrase_prefix" : {
"brand" : "johnnie walker bl"
}
}
次查询和match_phrase查询的工作方式基本相同,除了它会将查询字符串中的最后一个单词当做一个前缀。换言之,前面的例子会查找以下内容:
- johnnie
- 紧接着的是walker
- 紧接着的是以bl开头的单词
如果我们将该查询通过validate-query API执行,它会产生如下的解释:
"johnnie walker bl*"
和match_phrase查询一样,它能够接受一个slop参数(参见这里)来让单词间的顺序和相对位置不那么严格:
{
"match_phrase_prefix" : {
"brand" : {
"query": "walker johnnie bl",
"slop": 10
}
}
}
但是,查询字符串中的最后一个单词总是会被当做一个前缀。
在之前介绍prefix查询的时候,我们谈到了prefix查询的一些需要注意的地方 - prefix查询时如何消耗资源的。在使用match_phrase_prefix查询的时候,也面临着同样的问题。一个前缀a你能够匹配非常非常多的词条。匹配这么多的词条不仅会消耗很多资源,同时对于用户而言也是没有多少用处的。
我们可以通过将参数max_expansions设置成一个合理的数值来限制前缀扩展(Prefix Expansion)的影响,比如50:
{
"match_phrase_prefix" : {
"brand" : {
"query": "johnnie walker bl",
"max_expansions": 50
}
}
}
max_expansions参数会控制能够匹配该前缀的词条的数量。它会找到首个以bl开头的词条然后开始收集(以字母表顺序)直到所有以bl开头的词条都被遍历了或者得到了比max_expansions更多的词条。
五、部分匹配(Partial Matching)的ngrams
我们说过:"你只能找到存在于倒排索引中的词条"。尽管prefix,wildcard以及regexp查询证明了上面的说法并不是一定正确,但是执行一个基于单个词条的查询会比遍历词条列表来得到匹配的词条要更快是毫无疑问的。为了部分匹配而提前准备你的数据能够增加搜索性能。
在索引期间准别数据意味着选择正确的分析链(Analysis Chain),为了部分匹配我们选择的工具叫做n-gram。一个n-gram可以被想象成一个单词上的滑动窗口(Moving Window)。n表示的是长度。如果我们对单词quick得到n-gram,结果取决于选择的长度:
- 长度1(unigram): [ q, u, i, c, k ]
- 长度2(bigram): [ qu, ui, ic, ck ]
- 长度3(trigram): [ qui, uic, ick ]
- 长度4(four-gram):[ quic, uick ]
- 长度5(five-gram):[ quick ]
单纯的n-grams对于匹配单词中的某一部分是有用的,在复合单词的ngrams中我们会用到它。然而,对于即时搜索,我们使用了一种特殊的n-grams,被称为边缘n-grams(Edge n-grams)。边缘n-grams会将起始点放在单词的开头处。单词quick的边缘n-gram如下所示:
- q
- qu
- qui
- quic
- quick
你也许注意到它遵循了用户在搜索"quick"时的输入形式。换言之,对于即时搜索而言它们是非常完美的词条。
六、索引期间的即时搜索(Index-time Search-as-you-type)
建立索引期间即时搜索的第一步就是定义你的分析链(Analysis Chain)(在配置解析器中讨论过),在这里我们会详细阐述这些步骤:
准备索引
第一步是配置一个自定义的edge_ngram词条过滤器,我们将它称为autocomplete_filter:
{
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
}
}
以上配置的作用是,对于此词条过滤器接受的任何词条,它都会产生一个最小长度为1,最大长度为20的边缘ngram(Edge ngram)。
然后我们将该词条过滤器配置在自定义的解析器中,该解析器名为autocomplete。
{
"analyzer": {
"autocomplete": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"autocomplete_filter"
]
}
}
}
以上的解析器会使用standard分词器将字符串划分为独立的词条,将它们变成小写形式,然后为它们生成边缘ngrams,这要感谢autocomplete_filter。
创建索引,词条过滤器和解析器的完整请求如下所示:
PUT /my_index
{
"settings": {
"number_of_shards": 1,
"analysis": {
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
},
"analyzer": {
"autocomplete": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
}
你可以通过下面的analyze API来确保行为是正确的:
GET /my_index/_analyze?analyzer=autocomplete
quick brown
返回的词条说明解析器工作正常:
- q
- qu
- qui
- quic
- quick
- b
- br
- bro
- brow
- brown
为了使用它,我们需要将它适用到字段中,通过update-mapping API:
PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"name": {
"type": "string",
"analyzer": "autocomplete"
}
}
}
}
现在,让我们索引一些测试文档:
POST /my_index/my_type/_bulk
{ "index": { "_id": 1 }}
{ "name": "Brown foxes" }
{ "index": { "_id": 2 }}
{ "name": "Yellow furballs" }
查询该字段
如果你使用一个针对"brown fo"的简单match查询:
GET /my_index/my_type/_search
{
"query": {
"match": {
"name": {
"query": "brown fo",
"analyzer": "standard"
}
}
}
}
另外,还可以在name字段的映射中分别指定index_analyzer和search_analyzer。因为我们只是想修改search_analyzer,所以可以在不对数据重索引的前提下对映射进行修改:
PUT /my_index/my_type/_mapping
{
"my_type": {
"properties": {
"name": {
"type": "string",
"index_analyzer": "autocomplete",
"search_analyzer": "standard"
}
}
}
}
此时再通过validate-query API得到的解释如下:
GET /my_index/my_type/_validate/query?explain
{
"query": {
"match": {
"name": "brown fo"
}
}
}
name:brown name:fo
边缘ngrams和邮政编码
边缘ngrams这一技术还可以被用在结构化数据上,比如本章前面提到过的邮政编码。当然,postcode字段也许需要被设置为analyzed,而不是not_analyzed,但是你仍然可以通过为邮政编码使用keyword分词器来让它们和not_analyzed字段一样。所以对于一些通常被当做not_analyzed字段,然而需要某些处理(如转换为小写)的情况下,是有用处的。
这个例子使用keyword分词器将邮政编码字符串转换为一个字符流,因此我们就能够利用边缘ngram词条过滤器了:
{
"analysis": {
"filter": {
"postcode_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 8
}
},
"analyzer": {
"postcode_index": {
"tokenizer": "keyword",
"filter": [ "postcode_filter" ]
},
"postcode_search": {
"tokenizer": "keyword"
}
}
}
}
ES修改索引Setting信息方式:
PUT /my_temp_index/_settings
{
"number_of_replicas": 1
}