【云计算】elasticsearch关键词查询之实现like查询
小标 2018-12-24 来源 : 阅读 1288 评论 0

摘要:本文主要向大家介绍了【云计算】elasticsearch关键词查询之实现like查询,通过具体的内容向大家展现,希望对大家学习云计算有所帮助。

本文主要向大家介绍了【云计算】elasticsearch关键词查询之实现like查询,通过具体的内容向大家展现,希望对大家学习云计算有所帮助。


背景:我们项目需要对es索引里面的一个字段进行关键词(中文+英文+数字混合,中文偏多)搜索,相当于关系型数据库的like操作。要实现这个功能,我们首先想到的方式是用*通配符,但是实际应用场景查询语句会很复杂,*通配符的方式显得不够友好,导致慢查询,甚至内存溢出。


考虑到实际应用场景,一次查询会查询多个字段,我们项目采用query_string query方式,下面只考虑关键词字段。


数据准备


创建索引 es_test_index


PUT  127.0.0.1:9200/es_test_index
{
    "order": 0,
    "index_patterns": [
        "es_test_index"
    ],
    "settings": {
        "index": {
            "max_result_window": "30000",
            "refresh_interval": "60s",
            "number_of_shards": "3",
            "number_of_replicas": "1"
        }
    },
    "mappings": {
        "logs": {
            "_all": {
                "enabled": false
            },
            "properties": {
                "search_word": {
                    "type": "keyword"
                }
            }
        }
    }
}


方式一



{
    "profile":true,
    "from":0,
    "size":100,
    "query":{
        "query_string":{
            "query":"search_word:(*中国* NOT *美国* AND *VIP* AND *经济* OR *金融*)",
            "default_operator":"and"
        }
    }
}


采用*通配符的方式,相当于wildcard query,只是query_string能支持查询多个关键词,并且可以用 AND OR NOT进行连接,会更加灵活。



{
    "query": {
        "wildcard" : { "search_word" : "*中国*" }
    }
}


在我们的应用场景中,关键词前后都有*通配符,这个查询会非常慢,因为该查询需要遍历index里面的每个term。官方文档解释:Matches documents that have fields matching a wildcard expression (not analyzed). Supported wildcards are *, which matches any character sequence (including the empty one), and , which matches any single character. Note that this query can be slow, as it needs to iterate over many terms. In order to prevent extremely slow wildcard queries, a wildcard term should not start with one of the wildcards * or . 官方文档建议避免以*开头,但是我们要实现全匹配,前后都需要*通配符,可想而知效率是非常慢的。


在我们的实际项目中,我们发现用户有时候会输入很多个关键词,再加上其他的查询条件,单个查询的压力很大,导致了大量的超时。所以,我们决定换种方式实现like查询。


在仔细研究官方文档后,发现可以用standard分词+math_pharse查询实现。


重新创建索引



PUT  127.0.0.1:9200/es_test_index

{
    "order": 0,
    "index_patterns": [
        "es_test_index_2"
    ],
    "settings": {
        "index": {
            "max_result_window": "30000",
            "refresh_interval": "60s",
            "analysis": {
                "analyzer": {
                    "custom_standard": {
                        "type": "custom",
                        "tokenizer": "standard",
                        "char_filter": [
                            "my_char_filter"
                        ],
                        "filter": "lowercase"
                    }
                },
                "char_filter": {
                    "my_char_filter": {
                        "type": "mapping",
                        "mappings": [
                            "· => xxDOT1xx",
                            "+ => xxPLUSxx",
                            "- => xxMINUSxx",
                            "\" => xxQUOTATIONxx",
                            "( => xxLEFTBRACKET1xx",
                            ") => xxRIGHTBRACKET1xx",
                            "& => xxANDxx",
                            "| => xxVERTICALxx",
                            "—=> xxUNDERLINExx",
                            "/=> xxSLASHxx",
                            "!=> xxEXCLAxx",
                            "=> xxDOT2xx",
                            "【=>xxLEFTBRACKET2xx",
                            "】 => xxRIGHTBRACKET2xx",
                            "`=>xxapostrophexx",
                            ".=>xxDOT3xx",
                            "#=>xxhashtagxx",
                            ",=>xxcommaxx"
                        ]
                    }
                }
            },
            "number_of_shards": "3",
            "number_of_replicas": "1"
        }
    },
    "mappings": {
        "logs": {
            "_all": {
                "enabled": false
            },
            "properties": {
                "search_text": {
                    "analyzer": "custom_standard",
                    "type": "text"
                },
                "search_word": {
                    "type": "keyword"
                }
            }
        }
    }
}


注意看上面的索引,我创建了两个字段,search_word 跟方式一相同,为了对比两种方式的性能。 search_text :为了使用分析器,将type设置为text ,分析器设置为custom_standard 。


custom_standard组成:


字符过滤器char_filter:采用了mapping char filter 即接受原始文本作为字符流输入,把某些字符(自定义)转换为另外的字符。因为分词器采用了standard分词器,它会去掉大多数的符号,但是关键词搜索的过程可能会带有这些符号,如果去掉的话,会使搜索出来的结果不准确。比如 搜索 红+黄,分词之后 变成 红 黄,那么,搜索出来的结果可能包含 红+黄,红黄 ,而红黄并不是我们想要的。因此,运用字符过滤器,把+转换成字符串xxPLUSxx,那么在分词的时候,+就不会被去掉了。


分词器:standard 该分词器对英文比较友好,对于中文分词会分为单个字这样。


词元过滤器filter:lowercase 把分词过后的词元变为小写。


准备工作就绪,我们准备查询了,现在我们采用match_pharse查询方式。


方式二:



{
    "from": 0,
    "size": 100,
    "query": {
        "query_string": {
            "query": "search_text:(\"中国\" NOT \"美国\" AND \"VIP\" AND \"经济\" OR \"金融\")",
            "default_operator": "and"
        }
    }
}


我们来看下为什么match_phrase查询能实现关键词左右模糊匹配。


match_phrase 查询首先将查询字符串进行分词(如果不进行其他的参数设置,分词器采用创建索引时search_text字段的分词器custom_standard,如果不明白可以参考官方文档https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis.html),然后对这些词项进行搜索,但只保留那些包含 全部 搜索词项,且 位置 与搜索词项相同的文档。 换句话说,match_phrase查询不仅匹配字,还匹配位置。比如,search_text字段包含的内容是:当代中国正处于高速发展时期。 我们搜索关键词:中国


索引的时候 search_text经过分词器分为


我们可以用以下api查询分词效果



127.0.0.1:9200/es_test_index_2/_analyze

{
"analyzer": "custom_standard",
  "text":  "当代中国正处于高速发展时期"
}


返回结果:



{
    "tokens": [
        {
            "token": "当",
            "start_offset": 0,
            "end_offset": 1,
            "type": "",
            "position": 0
        },
        {
            "token": "代",
            "start_offset": 1,
            "end_offset": 2,
            "type": "",
            "position": 1
        },
        {
            "token": "中",
            "start_offset": 2,
            "end_offset": 3,
            "type": "",
            "position": 2
        },
        {
            "token": "国",
            "start_offset": 3,
            "end_offset": 4,
            "type": "",
            "position": 3
        },
        {
            "token": "正",
            "start_offset": 4,
            "end_offset": 5,
            "type": "",
            "position": 4
        },
        {
            "token": "处",
            "start_offset": 5,
            "end_offset": 6,
            "type": "",
            "position": 5
        },
        {
            "token": "于",
            "start_offset": 6,
            "end_offset": 7,
            "type": "",
            "position": 6
        },
        {
            "token": "高",
            "start_offset": 7,
            "end_offset": 8,
            "type": "",
            "position": 7
        },
        {
            "token": "速",
            "start_offset": 8,
            "end_offset": 9,
            "type": "",
            "position": 8
        },
        {
            "token": "发",
            "start_offset": 9,
            "end_offset": 10,
            "type": "",
            "position": 9
        },
        {
            "token": "展",
            "start_offset": 10,
            "end_offset": 11,
            "type": "",
            "position": 10
        },
        {
            "token": "时",
            "start_offset": 11,
            "end_offset": 12,
            "type": "",
            "position": 11
        },
        {
            "token": "期",
            "start_offset": 12,
            "end_offset": 13,
            "type": "",
            "position": 12
        }
    ]
}


我们可以看到经过分词之后,search_text会被分为单个的字并且还带有位置信息。位置信息可以被存储在倒排索引中,因此 match_phrase 查询这类对词语位置敏感的查询, 就可以利用位置信息去匹配包含所有查询词项,且各词项顺序也与我们搜索指定一致的文档,中间不夹杂其他词项。


在搜索的时候,关键词“中国”也会经过分词被分为“中” “国”两个字,然后 match_phrase 查询会在倒排索引中检查是否包含词项“中”和“国”并且“中”出现的位置只比“国”出现的位置大1。这样就刚好可以实现like模糊匹配。


实际上match_phrase查询会比简单的query查询更高,一个 match 查询仅仅是看词条是否存在于倒排索引中,而一个 match_phrase 查询是必须计算并比较多个可能重复词项的位置。Lucene nightly benchmarks 表明一个简单的 term 查询比一个短语查询大约快 10 倍,比邻近查询(有 slop 的短语 查询)大约快 20 倍。当然,这个代价指的是在搜索时而不是索引时。


通常,match_phrase 的额外成本并不像这些数字所暗示的那么吓人。事实上,性能上的差距只是证明一个简单的 term 查询有多快。标准全文数据的短语查询通常在几毫秒内完成,因此实际上都是完全可用,即使是在一个繁忙的集群上。


在某些特定病理案例下,短语查询可能成本太高了,但比较少见。一个典型例子就是DNA序列,在序列里很多同样的词项在很多位置重复出现。在这里使用高 slop 值会到导致位置计算大量增加。


下面我们来看看两种方式的查询效率:


我们用es_test_index_2 索引,里面 search_text是按照方式二定义的,search_word是按照方式一定义的,对两个字段导入相同的数据。


对该索引导入了25302条数据,11.3mb


方式一:*通配符



{
    "profile":true,
    "from":0,
    "size":100,
    "query":{
        "query_string":{
            "query":"search_word:(NOT *新品* AND *经典* OR *秒杀* NOT *预付*)",
            "fields": [],
            "type": "best_fields",
            "default_operator": "and",
            "max_determinized_states": 10000,
            "enable_position_increments": true,
            "fuzziness": "AUTO",
            "fuzzy_prefix_length": 0,
            "fuzzy_max_expansions": 50,
            "phrase_slop": 0,
            "escape": false,
            "auto_generate_synonyms_phrase_query": true,
            "fuzzy_transpositions": true,
            "boost": 1
        }
    }
}


方式二:match_phrase方式



{
    "from": 0,
    "size": 100,
    "query": {
        "query_string": {
            "query": "search_text:(NOT \"新品\" AND \"经典\" OR \"秒杀\" NOT \"预付\")",
            "fields": [],
            "type": "best_fields",
            "default_operator": "and",
            "max_determinized_states": 10000,
            "enable_position_increments": true,
            "fuzziness": "AUTO",
            "fuzzy_prefix_length": 0,
            "fuzzy_max_expansions": 50,
            "phrase_slop": 0,
            "escape": false,
            "auto_generate_synonyms_phrase_query": true,
            "fuzzy_transpositions": true,
            "boost": 1
        }
    }
}


查询结果:


方式一:



方式二:



从上面可以看出时间差别还是很大的,当需要查询的关键词很多的时候,优化效果会更好。大家可以自行去验证。


好啦,关键词like查询解决啦。


补充点:


一、


上述我们用的match_phrase查询属于精确匹配,即必须相邻才能被查出来。如果我们想要查询 “中国经济”,能让包含“中国当代经济”的文档也能查得出来,我们可以用match_phrase查询的参数 slop(默认为0) 来实现:—slop不为0的match_phrase查询称为邻近查询



{
    "from":0,
    "size":300,
    "query":{
        "match_phrase" : {
            "search_text" :
            {
                "query":"中国经济",
                "slop":2
            }
        }
    }
}


slop 参数告诉 match_phrase 查询词条相隔多远时仍然能将文档视为匹配 。 相隔多远的意思是为了让查询和文档匹配你需要移动词条多少次? 将slop设置成2 那么 包含“中国当代经济”的文档也能被查询出来。


在query_string query中可以这样写:



{
    "from": 0,
    "size": 100,
    "query": {
        "query_string": {
            "query": "search_text:(\"中国经济\"~2)",
            "default_operator": "and"
        }
   


当然你也可以运用query_string查询的参数 phrase_slop 来设置默认的slop的长度。详情参考https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html


二、


在使用短语查询的时候,会有一些意外的情况出现,比如:



PUT /my_index/groups/1
{
    "names": [ "John Abraham", "Lincoln Smith"]
}

或者

PUT /my_index/groups/1
{
    "names": "John Abraham, Lincoln Smith"
}


然后我们在运行一个Abraham Lincoln 短语查询的时候



GET /my_index/groups/_search
{
    "query": {
        "match_phrase": {
            "names": "Abraham Lincoln"
        }
    }
}


我们会发现文档会匹配到上述文档,实际上,我们不希望这样的匹配出现,字段names 不管是text数组形式,还是text形式,经过分词之后,都是John Abraham Lincoln Smith ,而Abraham Lincoln 属于相邻的,所以短语查询能够匹配到。


在这样的情况下,我们可以这样解决,将这个字段存为数组



DELETE /my_index/groups/ 

PUT /my_index/_mapping/groups 
{
    "properties": {
        "names": {
            "type":                "string",
            "position_increment_gap": 100
        }
    }
}


position_increment_gap 设置告诉 Elasticsearch 应该为数组中每个新元素增加当前词条 position 的指定值。 所以现在当我们再索引 names 数组时,会产生如下的结果:


* Position 1: john


* Position 2: abraham


* Position 103: lincoln


* Position 104: smith


现在我们的短语查询可能无法匹配该文档因为 abraham 和 lincoln 之间的距离为 100 。 为了匹配这个文档你必须添加值为 100 的 slop 。position_increment_gap默认是100.


另外,我们也可以在自定义分析器的时候设置该参数。



PUT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_custom_analyzer": {
          "type":      "custom",
          "tokenizer": "standard",
          "char_filter": [
            "html_strip"
          ],
          "filter": [
            "lowercase",
            "asciifolding"
          ],
         “position_increment_gap":101
        }
      }
    }
  }
}


          

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标大数据云计算大数据安全频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式AI+学习就业服务平台 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved