0.背景
整理了一下ES在具体搜索场景中的各种应用。
真实业务场景中,项目初期,PM整理出来的搜索需求对后端和算法都是小case,但是一旦到了红海阶段,各种复杂需求就出来了。
此次主要是对之前工作中用到的场景做一个整理。
1. ES搜索场景中常用查询类型简介
1.1 复合查询类型
在ES的语境下,符合查询可以理解为一种"筛选"。"筛选"和"搜索",主要区别是有没有涉及到分词,而是否分词又取决于field的类型
复合查询包装其他复合或子查询,以组合一次ES查询结果和打分(_score字段)或从查询切换到过滤上下文。
boolean查询
boolean查询是 匹配文档匹配其他查询的布尔组合的查询。 bool 查询映射到 Lucene BooleanQuery。 它是使用一个或多个布尔子句构建的,每个子句都有一个类型化的出现。
boolean涉及参数:
must:子句(查询)必须出现在匹配的文档中并参与打分。
filter:子句(查询)必须出现在匹配的文档中。 然而,与 must 不同的是,查询的分数将被忽略。 过滤器子句在过滤器上下文中执行,这意味着忽略评分并考虑缓存子句。换句话说。filter只用于无情的条件过滤,不会对打分结果有影响。
should:涉及字段应该包含查询条件(不参与打分)。
must_not:子句(查询)不得出现在匹配文档中。 子句在过滤器上下文中执行,这意味着评分被忽略并且子句被考虑用于缓存。 因为评分被忽略,所有文档的分数都为 0。
bool 查询采用匹配越多越好的方法,因此每个匹配的 must 或 should 子句的分数将被加在一起以提供每个文档的最终 _score。参考dsl示例:
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "user.id" : "kimchy" }
},
"filter": {
"term" : { "tags" : "production" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
},
"should" : [
{ "term" : { "tags" : "env1" } },
{ "term" : { "tags" : "deployed" } }
],
"minimum_should_match" : 1,
"boost" : 1.0
}
}
}
可以使用minimum_should_match 指定最小匹配数。
可以使用"."进行嵌套字段过滤。参考dsl:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "name.first": { "query": "shay", "_name": "first" } } },
{ "match": { "name.last": { "query": "banon", "_name": "last" } } }
],
"filter": {
"terms": {
"name.last": [ "banon", "kimchy" ],
"_name": "test"
}
}
}
}
}
Boosting 查询
返回与positive查询匹配的文档,同时降低与negative查询匹配的文档的相关性分数。
可以使用bossting查询来降级某些文档,而不会将它们从搜索结果中排除。
GET /_search
{
"query": {
"boosting": {
"positive": {
"term": {
"text": "apple"
}
},
"negative": {
"term": {
"text": "pie tart fruit crumble tree"
}
},
"negative_boost": 0.5
}
}
}
必填参数:positive和negative
简单说,positive用于指定具体条件,negaitive用于指定想要降低权重的数据的条件。
constant score 查询
包装filter查询并返回每个匹配的文档,其相关性得分等于 boost 参数值。
参考示例:
GET /_search
{
"query": {
"constant_score": {
"filter": {
"term": { "user.id": "kimchy" }
},
"boost": 1.2
}
}
}
此处的boost用于指定查询的分数。(具体打分逻辑有待后续完善)。
GET /_search
{
"query": {
"dis_max": {
"queries": [
{ "term": { "title": "Quick pets" } },
{ "term": { "body": "Quick pets" } }
],
"tie_breaker": 0.7
}
}
}
Disjunction max 查询
返回匹配一个或多个包装查询的文档,称为查询子句或子句。
简单说,如果需要匹配多个值,这些值在不同字段匹配的权重会比单个匹配权重的高。
如果返回的文档与多个查询子句匹配,则 dis_max 查询会为文档分配任何匹配子句中的最高相关性分数,以及任何其他匹配子查询的打破平局增量。
GET /_search
{
"query": {
"dis_max": {
"queries": [
{ "term": { "title": "Quick pets" } },
{ "term": { "body": "Quick pets" } }
],
"tie_breaker": 0.7
}
}
}
tie_breaker简介:
可以使用 tie_breaker 值为在多个字段中包含相同术语的文档分配比仅在这些多个字段中的最佳字段中包含该术语的文档更高的相关性分数,而不会将其与多个字段中两个不同术语的更好情况混淆 领域。
如果文档匹配多个子句,则 dis_max 查询计算文档的相关性分数如下:
- 从得分最高的匹配clause中获取相关性得分。
- 将任何其他匹配cluase的分数乘以 tie_breaker 值。
- 将最高分加到相乘的分数上。
- 如果 tie_breaker 值大于 0.0,则所有匹配的子句都有效,但得分最高的子句权重最高
Function Score查询
TODO:后续待补充。
1.2全文搜索类型
全文查询用于搜索经过分析和分词的文本字段,比如:根据某个姓搜索姓名,根据某个词搜索文章,tec。 全文搜索时候使用的分词器和建立索引时候的一致。
match:
返回与提供的text、number、date或bool匹配的文档。 在match之前分析提供的文本。
match查询是执行全文搜索的标准查询,包括模糊匹配选项。
参考DSL:
GET /_search
{
"query": {
"match": {
"message": {
"query": "this is a test"
}
}
}
}
match是一个很常用的DSL查询关键字,能满足很多基础的查询需求。一般应用场景下都是针对text场景进行搜索(虽然支持number、date和bool,但是这时候用term更精确,很难说意义多大)。
使用match关键字,需要指定对应的field为text、number、date或bool。不要使用keyword类型的field进行匹配
另外对text一般在建表时候需要指定analyzer(可以理解为分词器)。如果不指定analyzer,ES会调用默认的分词器进行分词。
match的参数:
query(required):构造基本的查询需求;
analyzer(optional):指定分词器。这个需求可以满足以下场景:ES中存储了多个分词器分词的结果,紧急需求需要强化屏蔽。可以在query的时候指定分词器为带有更多停用词的分词器。当然这个有一个前提:在建立ES索引的时候已经有这个分词器,否则需要重新刷数据。
auto_generate_synonyms_phrase_query(optional):是否自动生成同义词查询。比如搜索"ny",是否要自动关联"New York"。
fuzziness(optional):是否支持模糊查询。一般建议关闭,有点类似mysql中的like查询,对性能损耗较大。
max_expansions(optional):匹配term时候涉及到的最大match,最大50。个人理解是最大match匹配数(这块待研究,欢迎指正)。
prefix_length(optional):模糊匹配时候必须匹配的前缀字符数,默认50
fuzzy_transpositions(optional):如果为true,则模糊匹配的编辑包括两个相邻字符 (ab → ba) 的换位。 默认为true。这块个人也是不太建议开启,如果想要实现这个功能,可以在算法侧实现搜索联想功能和自动纠错功能。
lenient(optional):如果为true,则忽略基于格式的错误,例如为numberic字段提供text查询值会被忽略。 默认为false。
minimum_should_match(optional):最小匹配数,指定需要最小匹配的word的数目。
zero_terms_query(optional):提供"all"和"none"两种模式。如果为"none",则在分词器移除了所有token之后,不返回结果。"all"模式下,类似"match_all"。
match_bool_prefix
match_bool_prefix 查询会分析其输入,并根据输入的词语,构造类似term的 bool 查询。 除了最后一个term外,每个term都用于term查询。 最后一个term用于前缀查询。
GET /_search
{
"query": {
"match_bool_prefix" : {
"message" : "quick brown f"
}
}
}
实现效果类似下述dsl
GET /_search
{
"query": {
"bool" : {
"should": [
{ "term": { "message": "quick" }},
{ "term": { "message": "brown" }},
{ "prefix": { "message": "f"}}
]
}
}
}
这里可能有点难以理解:term不是精确查询嘛,match_bool_prefix的效果还算是一种搜索而不是筛选嘛?这个问题可以这么理解:上述第二个dsl针对的并不是txt原文本,而是针对分词后的结果,分词后的text类似一个string list。因此官方文档也提到过match_bool_prefix和match_phrase_prefix的区别:
match_bool_prefix 查询和 match_phrase_prefix 之间的一个重要区别:
match_phrase_prefix 查询将其term作为短语进行匹配,而 match_bool_prefix 查询可以在任何位置匹配其term。 上面的示例 match_bool_prefix 查询可以搜索到 "quick brown fox "的信息,也可以匹配 "brown fox quick"。 它还可以匹配包含quick、brown 和以 f 开头且出现在任何位置的term的文本。
match_phrase
match_phrase 查询分词后的text并根据分词的文本创建term查询。 例如:
phrase查询以任何顺序将term匹配到可配置的slop(默认为 0)。
GET /_search
{
"query": {
"match_phrase": {
"message": "this is a test"
}
}
}
可以设置analyzser来控制那哪个分词器将对文本执行分词过程。 它默认为字段显式映射定义,或默认搜索分析器,例如:
GET /_search
{
"query": {
"match_phrase": {
"message": {
"query": "this is a test",
"analyzer": "my_analyzer"
}
}
}
}
match_phrase_prefix
match_phrase_prefix 返回包含所提供文本单词的文档,顺序与提供的顺序相同。 所提供文本的最后一个术语被视为前缀,匹配以该术语开头的任何单词。
dsl示例:
以下搜索返回消息字段中包含以 quick brown f 开头的短语的文档。
此搜索可以匹配message: "quick brown fox" 和"two quick brown ferrets",但匹配到"the fox is quick and brown"
GET /_search
{
"query": {
"match_phrase_prefix": {
"message": {
"query": "quick brown f"
}
}
}
}
match_phrase_prefix 参数:query,analyzer,slop,max_expansions,zero_terms_query
这些和match类似,不做过多解释。
combined_fields
combined_fields 查询支持搜索多个文本字段,就好像它们的内容已被索引到一个组合字段中一样。 该查询采用以term为中心的输入字符串视图:首先它将查询字符串分析为单独的词,然后在任何字段中查找每个词。 当匹配可能跨越多个文本字段(例如文章的标题、摘要和正文)时,此查询特别有用:
GET /_search
{
"query": {
"combined_fields" : {
"query": "database systems",
"fields": [ "title", "abstract", "body"],
"operator": "and"
}
}
}
combined_fields 查询采用基于概率相关性框架:BM25 及以后中描述的简单 BM25F 公式的原则性评分方法。 在对匹配项进行评分时,查询会跨字段组合术语和集合统计信息来对每个匹配项进行评分,就好像指定字段已被索引到单个组合字段中一样。
可以对每个关键字设置权重,参考dsl:
GET /_search
{
"query": {
"combined_fields" : {
"query" : "distributed consensus",
"fields" : [ "title^2", "body" ]
}
}
}
与 multi_match 查询的比较
combined_fields 查询提供了一种跨多个文本字段进行匹配和评分的原则性方法。 为了支持这一点,它要求所有字段都具有相同的analyzer。
如果您想要一个处理不同类型字段(如text或keyword)的查询,那么 multi_match 查询可能更合适。 它支持text和非text字段,并接受不共享同一analyzer的文本字段。
例如,下述的combined_fields查询:
GET /_search
{
"query": {
"combined_fields" : {
"query": "database systems",
"fields": [ "title", "abstract"],
"operator": "and"
}
}
}
执行时候的逻辑类似:
+(combined("database", fields:["title" "abstract"]))
+(combined("systems", fields:["title", "abstract"]))
换句话说,每个term必须至少出现在一个field中才能匹配文档。
补充: multi_match 模式 best_fields 和 most_fields 采用以字段为中心的查询视图。 相比之下,combined_fields 是以术语为中心的:operator 和 minimum_should_match 应用于每个术语,而不是每个字段。
cross_fields multi_match 模式也采用以term为中心的方法,并为每个term应用 operator 和 minimum_should_match。 combined_fields 相对于 cross_fields 的主要优势是其基于 BM25F 算法的强大且可解释的评分方法。
multi_match
multi_match是基于match查询的关键字。dsl示例:
GET /_search
{
"query": {
"multi_match" : {
"query": "this is a test",
"fields": [ "subject", "message" ]
}
}
}
可以使用wildcard正则表达式。
GET /_search
{
"query": {
"multi_match" : {
"query": "Will Smith",
"fields": [ "title", "*_name" ]
}
}
}
可以使用"^"进行打分加权。
GET /_search
{
"query": {
"multi_match" : {
"query" : "this is a test",
"fields" : [ "subject^3", "message" ]
}
}
}
如果没有提供"fields"字段的值,则 multi_match 查询默认为 index.query.default_field 索引设置,而后者又默认为 *。 * 提取映射中符合术语查询条件的所有字段并过滤元数据字段。 然后组合所有提取的字段以构建查询。
multi_match的query类型:
best_fields:默认类型。查找与任何field匹配的documents,返回时候根据_score排序(es内部打分字段)。
best_fields的dsl示例:
GET /_search
{
"query": {
"multi_match" : {
"query": "brown fox",
"type": "best_fields",
"fields": [ "subject", "message" ],
"tie_breaker": 0.3
}
}
}
most_fields:查找与任何field匹配的文档,并结合每个字段的 _score。
cross_fields:针对query字段在不同fields进行匹配查询,比如查询"will smith",查询字段为"name"和"last name",那么"name"有"will"并且"last name"有"smith"的文档比"name"都包含了"will"和"smith"的权重更高。
phrase:对每个字段运行 match_phrase 查询并使用最佳字段的 _score。
phrase_prefix:在每个字段上运行 match_phrase_prefix 查询并使用来自最佳字段的 _score。
bool_prefix:在每个字段上创建一个 match_bool_prefix 查询并组合每个字段的 _score。
analyzer, boost, operator, minimum_should_match, lenient, zero_terms_query, 和 auto_generate_synonyms_phrase_query这些参数在multi_match中都能使用,具体使用参考match。
query_string和simple_query_string
使用具有严格语法的解析器,根据提供的查询字符串返回文档。
此查询使用语法根据运算符(例如 AND 或 NOT)解析和拆分提供的查询字符串。 然后,查询在返回匹配文档之前独立分析每个拆分文本。(研究不多,后续补充)。
intervals
查询
可以简单理解为嵌套查询。本文不做过多分析。
示例DSL:
POST _search
{
"query": {
"intervals" : {
"my_text" : {
"all_of" : {
"ordered" : true,
"intervals" : [
{
"match" : {
"query" : "my favorite food",
"max_gaps" : 0,
"ordered" : true
}
},
{
"any_of" : {
"intervals" : [
{ "match" : { "query" : "hot water" } },
{ "match" : { "query" : "cold porridge" } }
]
}
}
]
}
}
}
}
}
2. 具体场景应用
2.1 确定是否需要使用ES
首先根据需求判断是否要上ES。
如果现有数据都基于Mysql,like查询+索引优化就能满足条件,不建议再上ES。
如果查询数据基于ClickHouse查询,并且无高并发需求,主要是报表场景,也无需上ES,CK已经对查询做了一系列优化,稍微复杂的like查询也不会有很多耗时。
如果对搜索有强要求,并且是like查询无法满足的需求还是要上ES。
2.2 简单需求场景与解决方法
以下列出一些关于具体需求场景的dsl使用。
背景:某论坛有了大量的帖子,之前的搜索做的不好,现在需要优化搜索。
场景1:
最简单需求,输入关键字,查到包含该关键字的文章,无打分需求。
解决方法:建立索引时候指定分词器,文章入库时候会自动进行分词。使用match或者multi_match进行查询即可
场景2:
在场景1的基础上,支持对文章类型进行筛选,比如筛选游戏、旅游、数码,etc;支持时间范围筛选,例如从一年前至今。
解决方法:刷库时候需要指定文章类别,脏数据用各种方法进行更新(比如全量打标,etc)。查询dsl使用match或者multi_match,结合boolean筛选(类别筛选使用filter +term,时间范围筛选使用filer+range筛选)。
场景3:
场景2的基础上,时间范围半年内的优先展示。
解决方法:在场景2的term中使用boosting_query,positive中过滤类型,negative的条件填写发表时间在半年上,具体的negative_boost根据效果而定。
场景4:
搜索联想,比如在搜索框输入了"高达",会联想"高达0079","高达Z","高达00"等;
解决方法:使用match_phrase_prefix或者match_bool_prefix,或者multi_match的phrase类型搜索。
场景5:
如果输入多个搜索联想词,比如"高达 超时空要塞 变形",会联想“"高达 超时空要塞 变形金刚","高达和变形金刚傻傻分不清"
解决方法:使用match_bool_prefix查询,使用multi_match的phrase和phrase_fix类型也可以。
场景6:
帖子在ES存放,细化为了"title"(标题),"content"(帖子内容),"tag"(标签)等多个字段。产品想在搜索结果中优先推荐标题命中输入词的结果。
解决方法:使用多次match,分别match "title","content"和"tag"。对match"title"进行加权打分,使用boost指定"title"的权重。
场景7:
情人节来临,产品希望"情感"类型的、并且包含"情人节"和"秀恩爱"、并且发表时间在最近一个月内的帖子得到更多推广;另外发表时间不在最近一个月的降权。
解决方法:首先使用"filer"+"term"筛选"情感"类型帖子,使用"filter"+"range"过滤发表时间,match或者multi_match来匹配"情人节"和"秀恩爱"等字段,最后使用function_score对整个条件进行加权打分;构造另一个包含"情感"类型和match"情人节""秀恩爱"的查询,不使用function_score降权。
场景8:
产品需求:如果输入词分别命中标题和文章,比全命中文章或者全命中标题的优先级高。
解决方法:使用multi_match的cross_fields。
场景9:
需求:论坛有定制化分词需求,比如分词结果希望多出现"高达","扎古"(高达中的机体名字),"杰钢"(高达中的机体名字),"夏亚"(高达人物名),"阿姆罗"(高达人物名),在此基础上优化搜索。
解决方法:首先需要自定义分词器;之后重新建立索引,添加自定义分词器(原有分词器可以不删除);之后异步刷库,并在上线时候切换索引;最后在查询时候指定分词器。
2.3 高级场景以及解决方法
在2.2节场景上产品提出一些更复杂的思路。
场景1:
基于近义词和同义词的搜索,比如搜索"淘宝",需要搜索出"阿里"相关的结果。
解决方案:需要联系算法同事进行语义分析和联想,并在此基础上再细分搜索查询类型。
场景2:
相关搜索场景,比如搜索"高达","相关搜索"栏出现"高达模型热卖"
解决方法:新加一路召回,进行相关召回。相关召回可以直接交给算法,部署相关搜索模型。如果算法人力不够,可以只提供相关搜索联想词,工程去查询ES或其他语料库
3. 优化思路
简单写几个优化思路。
1. 可以使用redis缓存搜索结果;
2. 并发数过多,ES不堪重负,简单的可以加机器,或者进行分片;
3. 尽量少使用模糊查询和正则匹配;
4. 如果有多个搜索链路(即多路召回),使用并发召回;
5. 只筛选的字段不需要指定为text,keyword类型即可,减少分词带来的内存消耗和磁盘消耗;
6. 使用规则引擎管理搜索规则;或者使用mysql,consul等中间件管理搜索规则;
7. 合理规划es的内存,单台ES机器的内存不建议超过31G(超过32G,Java就无法使用压缩指针技术);
其他后续补充。文章来源:https://www.toymoban.com/news/detail-480484.html
4. 参考文献
Match query | Elasticsearch Guide [8.7] | Elastic文章来源地址https://www.toymoban.com/news/detail-480484.html
到了这里,关于ES 搜索场景具体应用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!