关于elasticsearch的query_string通用搜索方案改造
在实际使用elasticsearch做搜索引擎的业务场景中,我们经常会被很多组合条件弄得晕头转向。如果在业务中使用JSON来做搜索条件的处理,你会发现调用客户端api的同事,需要跟你一样精通搜索语法,带来了额外的沟通和学习成本。
elasticsearch官方支持了query_string这种直观简洁的搜索语法,从而成为了我们做搜索业务的首选。
话不多说,我们来看一下query_string相关的搜索语法示例:
{
"query": {
"query_string": {
"query": "province: ('31' OR '32') AND goods:'小馒头包子'",
"default_operator": "AND",
"allow_leading_wildcard": false,
"analyze_wildcard": false
}
}
}
从上面的语法可以看出,相对于JSON来说,其协议十分简洁直观,值得细品。
而要想达成上面的效果,其实我们还有很多事情要做。接下来,小编带大家一起来踩坑啦!
1,索引settings,如何配置?mapping如何设计?
2,排序TOPN,如何设计?
上面两个难题,相信做过搜索的人都经历过。
开始前,我们先来认识一下es常用的数据类型。
Numeric: int, long, short, float, double等等数字类型
Keyword: 关键字类型
Text: 文本类型
es中没有数组类型,但是以上常见的数据类型,都可以直接传数组(参考:官方对Arrays的解释)
以上三种类型都支持范围查询语法。但一般来说,最好用Numeric类型的字段来做范围查询比较合理。
Keyword类型是一种特殊类型,所有定义为keyword类型的字段,都只能全量匹配。同时keyword与numeric有着一定的联系,下面是官方的解释:
简而言之,就是说需要用到范围查询的,尽量定义为numeric类型,不需要范围查询,只是匹配的,完全可以用keyword更加高效。
text文本类型是用来做分词的,不需要分词的就直接用keyword就行。
从这里可以看出,keyword实际上就是一个不可分割的term,term query场景下非常合适。
有了以上概念之后,我们开始进入今天的正题。
我们举一个简单的搜索场景示例:搜索淘宝商品功能。
我们先定一个商品索引,字段有:省份,商品名称,店铺名称,价格
索引配置如下:
{
"mappings": {
"dynamic": "strict",
"properties": {
"province": {
"type": "keyword",
"normalizer": "keyword"
},
"goods": {
"type": "text",
"analyzer": "chn_standard",
"copy_to": ["search"]
},
"name": {
"type": "text",
"analyzer": "chn_standard",
"copy_to": ["search"]
},
"price": {
"type": "double"
},
"search": {
"type": "text",
"analyzer": "chn_standard"
}
}
},
"settings": {
"index": {
"mapping": {
"coerce": "false"
},
"analysis": {
"filter": {
"graph_synonyms": {
"type": "synonym_graph",
"synonyms_path": "analysis/synonym.txt",
"lenient": "true"
},
"english_stop": {
"type": "stop",
"stopwords": "_english_"
}
},
"char_filter": {
"punctuation": {
"type": "mapping",
"mappings": [
"'=>",
"\"=>"
]
}
},
"normalizer": {
"keyword": {
"filter": [
"lowercase",
"asciifolding"
],
"type": "custom",
"char_filter": [
"punctuation"
]
}
},
"analyzer": {
"chn_standard": {
"filter": [
"graph_synonyms",
"lowercase"
],
"char_filter": [
"punctuation"
],
"type": "custom",
"tokenizer": "hanlp_smart"
}
},
"tokenizer": {
"hanlp_smart": {
"enable_stop_dictionary": "true",
"enable_custom_config": "true",
"enable_place_recognize": "true",
"type": "hanlp_standard",
"enable_offset": "true"
}
}
},
"max_rescore_window": "1000000"
}
}
}
小编这里使用的是hanlp分词库,来做中文分词。hanlp插件 需要安装。
看上面的索引配置,mapping中定义的字段及其类型都按照文章开头的描述定义好了。
其中有个search字段是(goods, name)组成的组合字段,组合字段的好处就是,search: '小馒头' 就相当于 goods: '小馒头' OR name:'小馒头'
一个查询可以匹配多个字段。
对于keyword类型字段,例如:province,录入的时候最好使用数字替代中文,至于原因自行体会(上海,上海市)。
我们重点来关注一下analysis里面的配置项。
filter里面定义了同义词映射,英文停用词,
charfilter里面定义了punctuation特定标点替换,为的就是能够支持带引号的搜索,es里面如果直接搜索province: '31'是不能匹配到province为31的文档的,因为它去匹配了'31'。需要去掉引号搜索,char_filter就提供了这种功能。
keyword类型没有analyzer配置项,官方提供的解决方案是配置normalizer项,详情见上面的配置。字段定义的时候再配置 "normalizer": "keyword",即可支持带引号的匹配。
analyzer中我们定义了chn_standard,就是标准的hanlp中文分词。
在我们搜索goods: '小馒头包子' 的时候,es会根据search_analyzer指定的分词器对'小馒头包子'分词,文章开头有一个搜索示例,其中增加了一个参数default_operator: "AND", 其作用就是匹配goods字段中既包含‘小馒头’ ,又包含‘包子'的文档。default_operator参数默认是“OR”
,不指定就会导致搜索结果不精确。
至此,我们就解决了索引设计相关的常见问题,希望对大家有所帮助。
下面,我们重点来说一说TOPN排序场景应该如何设计。
大部分的排序场景就是,从海量的文档中选取我们最感兴趣的前N条文档返回结果。
排序需要分阶段进行,不知大家有没有注意到上面的配置,有个参数:max_rescore_window
这个就是用来限制召回文档粗略排序的量级。我们所要做的事情就是对搜索引擎匹配到的这100万个相关的文档进行TOPN排序。
但问题来了,如此大批量的文档排序会有很大的性能损失。最好的解决方案,就是利用数值类型字段进行粗略的排序,数值计算和排序不会消耗太多服务器资源。然后对粗排后的结果,再进行TOPN精确排序,这样就能很大程度上保证,前N条文档的搜索准确性。
搜索请求body如下:
我们使用function_score的script_score来做粗排,rescore来做精确排序。注意rescore的window_size必须要跟索引主分片总量关联起来。window_size表示的是单个索引分片参与排序文档数。例如:主分片个数为15,则参与rescore的文档总数为15*100。track_total_hits参数用来限制统计匹配文档总数的量级。当数量超过track_total_hits,es只会返回gte: track_total_hits,而不是实际的总数。所以尽量不要用es来做总量统计,出于性能方面的考虑,它只能给你一个估算值。
{
"size": "5",
"from": "0",
"query": {
"function_score": {
"query": {
"query_string": {
"query": "search:'小馒头包子' AND province:'31'",
"allow_leading_wildcard": false,
"analyze_wildcard": false,
"default_operator": "AND"
}
},
"script_score": {
"script": {
"lang": "painless",
"source": "_score*500+price*10"
}
}
}
},
"rescore": [
{
"window_size": 100,
"external": {
"factor": 1,
"keyword": "小馒头包子",
"formula": "text_relevance(goods)*10000+text_relevance(name)*10000",
"explain": false
}
}
],
"highlight": {
"order": "score",
"number_of_fragments": 1,
"type": "fvh",
"require_field_match": false,
"fragment_size": 120,
"pre_tags": [
"<em>"
],
"post_tags": [
"</em>"
],
"fields": {
"goods": {
"fragment_size": "256"
},
"name": {
"fragment_size": "256"
}
}
},
"_source": [
"name",
"goods"
],
"sort": [
{
"_score": {
"order": "desc"
}
}
],
"track_total_hits": 10000000
}
以上就是小编在es搜索使用方面的经验总结,觉得有帮助的帮忙点波赞哦!!!