elasticsearch 关键词查询-实现like查询(转)

来源: https://blog.csdn.net/qq_22612245/article/details/82432107

背景:我们项目需要对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": "<IDEOGRAPHIC>",

"position": 0

},

{

"token": "代",

"start_offset": 1,

"end_offset": 2,

"type": "<IDEOGRAPHIC>",

"position": 1

},

{

"token": "中",

"start_offset": 2,

"end_offset": 3,

"type": "<IDEOGRAPHIC>",

"position": 2

},

{

"token": "国",

"start_offset": 3,

"end_offset": 4,

"type": "<IDEOGRAPHIC>",

"position": 3

},

{

"token": "正",

"start_offset": 4,

"end_offset": 5,

"type": "<IDEOGRAPHIC>",

"position": 4

},

{

"token": "处",

"start_offset": 5,

"end_offset": 6,

"type": "<IDEOGRAPHIC>",

"position": 5

},

{

"token": "于",

"start_offset": 6,

"end_offset": 7,

"type": "<IDEOGRAPHIC>",

"position": 6

},

{

"token": "高",

"start_offset": 7,

"end_offset": 8,

"type": "<IDEOGRAPHIC>",

"position": 7

},

{

"token": "速",

"start_offset": 8,

"end_offset": 9,

"type": "<IDEOGRAPHIC>",

"position": 8

},

{

"token": "发",

"start_offset": 9,

"end_offset": 10,

"type": "<IDEOGRAPHIC>",

"position": 9

},

{

"token": "展",

"start_offset": 10,

"end_offset": 11,

"type": "<IDEOGRAPHIC>",

"position": 10

},

{

"token": "时",

"start_offset": 11,

"end_offset": 12,

"type": "<IDEOGRAPHIC>",

"position": 11

},

{

"token": "期",

"start_offset": 12,

"end_offset": 13,

"type": "<IDEOGRAPHIC>",

"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

}

}

}

}

}

参考文档:

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-custom-analyzer.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-tokenizer.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-mapping-charfilter.html

---------------------

作者:stellaYdc

来源:CSDN

转载自:https://blog.csdn.net/qq_22612245/article/details/82432107

© 著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama 阅读 157,612 评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama 阅读 66,814 评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人 阅读 107,427 评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人 阅读 43,743 评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事 阅读 52,104 评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人 阅读 40,455 评论 1 214
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama 阅读 31,764 评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人 阅读 30,454 评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama 阅读 34,159 评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事 阅读 30,446 评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事 阅读 31,953 评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama 阅读 28,294 评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事 阅读 32,927 评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人 阅读 26,028 评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人 阅读 26,784 评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama 阅读 35,485 评论 2 270
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事 阅读 35,395 评论 2 264

推荐阅读 更多精彩内容