Term Query Filter_关于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有着一定的联系,下面是官方的解释:

310030034bc49d16abdea3c499b21df9.png

简而言之,就是说需要用到范围查询的,尽量定义为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": [ "" ], "post_tags": [ "" ], "fields": { "goods": { "fragment_size": "256" }, "name": { "fragment_size": "256" } } }, "_source": [ "name", "goods" ], "sort": [ { "_score": { "order": "desc" } } ], "track_total_hits": 10000000 }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值