elasticsearch 关键词查询-实现like查询
背景:我們項目需要對es索引里面的一個字段進行關鍵詞(中文+英文+數(shù)字混合,中文偏多)搜索,相當于關系型數(shù)據(jù)庫的like操作。要實現(xiàn)這個功能,我們首先想到的方式是用*通配符,但是實際應用場景查詢語句會很復雜,*通配符的方式顯得不夠友好,導致慢查詢,甚至內(nèi)存溢出。
考慮到實際應用場景,一次查詢會查詢多個字段,我們項目采用query_string query方式,下面只考慮關鍵詞字段。
數(shù)據(jù)準備
創(chuàng)建索引 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 *經(jīng)濟* 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 ?. 官方文檔建議避免以*開頭,但是我們要實現(xiàn)全匹配,前后都需要*通配符,可想而知效率是非常慢的。
在我們的實際項目中,我們發(fā)現(xiàn)用戶有時候會輸入很多個關鍵詞,再加上其他的查詢條件,單個查詢的壓力很大,導致了大量的超時。所以,我們決定換種方式實現(xiàn)like查詢。
在仔細研究官方文檔后,發(fā)現(xiàn)可以用standard分詞+math_pharse查詢實現(xiàn)。
重新創(chuàng)建索引
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"}}}} }注意看上面的索引,我創(chuàng)建了兩個字段,search_word 跟方式一相同,為了對比兩種方式的性能。 search_text :為了使用分析器,將type設置為text ,分析器設置為custom_standard 。
custom_standard組成:
字符過濾器char_filter:采用了mapping char filter 即接受原始文本作為字符流輸入,把某些字符(自定義)轉(zhuǎn)換為另外的字符。因為分詞器采用了standard分詞器,它會去掉大多數(shù)的符號,但是關鍵詞搜索的過程可能會帶有這些符號,如果去掉的話,會使搜索出來的結(jié)果不準確。比如 搜索 紅+黃,分詞之后 變成 紅 黃,那么,搜索出來的結(jié)果可能包含 紅+黃,紅黃 ,而紅黃并不是我們想要的。因此,運用字符過濾器,把+轉(zhuǎn)換成字符串xxPLUSxx,那么在分詞的時候,+就不會被去掉了。
分詞器:standard? 該分詞器對英文比較友好,對于中文分詞會分為單個字這樣。
詞元過濾器filter:lowercase? 把分詞過后的詞元變?yōu)樾憽?/p>
準備工作就緒,我們準備查詢了,現(xiàn)在我們采用match_pharse查詢方式。
方式二:
{"from": 0,"size": 100,"query": {"query_string": {"query": "search_text:(\"中國\" NOT \"美國\" AND \"VIP\" AND \"經(jīng)濟\" OR \"金融\")","default_operator": "and"}} }我們來看下為什么match_phrase查詢能實現(xiàn)關鍵詞左右模糊匹配。
?
match_phrase 查詢首先將查詢字符串進行分詞(如果不進行其他的參數(shù)設置,分詞器采用創(chuàng)建索引時search_text字段的分詞器custom_standard,如果不明白可以參考官方文檔https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis.html),然后對這些詞項進行搜索,但只保留那些包含 全部 搜索詞項,且 位置 與搜索詞項相同的文檔。 換句話說,match_phrase查詢不僅匹配字,還匹配位置。比如,search_text字段包含的內(nèi)容是:當代中國正處于高速發(fā)展時期。? ? 我們搜索關鍵詞:中國??
索引的時候 search_text經(jīng)過分詞器分為
我們可以用以下api查詢分詞效果
127.0.0.1:9200/es_test_index_2/_analyze{ "analyzer": "custom_standard","text": "當代中國正處于高速發(fā)展時期" }返回結(jié)果:
{"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": "發(fā)","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}] }我們可以看到經(jīng)過分詞之后,search_text會被分為單個的字并且還帶有位置信息。位置信息可以被存儲在倒排索引中,因此 match_phrase 查詢這類對詞語位置敏感的查詢, 就可以利用位置信息去匹配包含所有查詢詞項,且各詞項順序也與我們搜索指定一致的文檔,中間不夾雜其他詞項。
在搜索的時候,關鍵詞“中國”也會經(jīng)過分詞被分為“中”? “國”兩個字,然后 match_phrase 查詢會在倒排索引中檢查是否包含詞項“中”和“國”并且“中”出現(xiàn)的位置只比“國”出現(xiàn)的位置大1。這樣就剛好可以實現(xiàn)like模糊匹配。
實際上match_phrase查詢會比簡單的query查詢更高,一個 match 查詢僅僅是看詞條是否存在于倒排索引中,而一個 match_phrase 查詢是必須計算并比較多個可能重復詞項的位置。Lucene nightly benchmarks 表明一個簡單的 term 查詢比一個短語查詢大約快 10 倍,比鄰近查詢(有 slop 的短語 查詢)大約快 20 倍。當然,這個代價指的是在搜索時而不是索引時。
通常,match_phrase 的額外成本并不像這些數(shù)字所暗示的那么嚇人。事實上,性能上的差距只是證明一個簡單的 term 查詢有多快。標準全文數(shù)據(jù)的短語查詢通常在幾毫秒內(nèi)完成,因此實際上都是完全可用,即使是在一個繁忙的集群上。
在某些特定病理案例下,短語查詢可能成本太高了,但比較少見。一個典型例子就是DNA序列,在序列里很多同樣的詞項在很多位置重復出現(xiàn)。在這里使用高 slop 值會到導致位置計算大量增加。
下面我們來看看兩種方式的查詢效率:
我們用es_test_index_2 索引,里面 search_text是按照方式二定義的,search_word是按照方式一定義的,對兩個字段導入相同的數(shù)據(jù)。
對該索引導入了25302條數(shù)據(jù),11.3mb
方式一:*通配符
{"profile":true,"from":0,"size":100,"query":{"query_string":{"query":"search_word:(NOT *新品* AND *經(jīng)典* 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 \"經(jīng)典\" 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}} }查詢結(jié)果:
方式一:
? ? ? ? ? ? ? ?
方式二:
?? ??
從上面可以看出時間差別還是很大的,當需要查詢的關鍵詞很多的時候,優(yōu)化效果會更好。大家可以自行去驗證。
好啦,關鍵詞like查詢解決啦。
補充點:
一、
上述我們用的match_phrase查詢屬于精確匹配,即必須相鄰才能被查出來。如果我們想要查詢 “中國經(jīng)濟”,能讓包含“中國當代經(jīng)濟”的文檔也能查得出來,我們可以用match_phrase查詢的參數(shù) slop(默認為0) 來實現(xiàn):—slop不為0的match_phrase查詢稱為鄰近查詢
{"from":0,"size":300,"query":{"match_phrase" : {"search_text" :{"query":"中國經(jīng)濟","slop":2}}} }slop 參數(shù)告訴 match_phrase 查詢詞條相隔多遠時仍然能將文檔視為匹配 。 相隔多遠的意思是為了讓查詢和文檔匹配你需要移動詞條多少次? 將slop設置成2 那么 包含“中國當代經(jīng)濟”的文檔也能被查詢出來。
在query_string query中可以這樣寫:
{"from": 0,"size": 100,"query": {"query_string": {"query": "search_text:(\"中國經(jīng)濟\"~2)","default_operator": "and"}當然你也可以運用query_string查詢的參數(shù) phrase_slop 來設置默認的slop的長度。詳情參考https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html
二、
在使用短語查詢的時候,會有一些意外的情況出現(xiàn),比如:
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"}} }我們會發(fā)現(xiàn)文檔會匹配到上述文檔,實際上,我們不希望這樣的匹配出現(xiàn),字段names 不管是text數(shù)組形式,還是text形式,經(jīng)過分詞之后,都是?John Abraham ?Lincoln Smith ?,而?Abraham ?Lincoln 屬于相鄰的,所以短語查詢能夠匹配到。
在這樣的情況下,我們可以這樣解決,將這個字段存為數(shù)組
DELETE /my_index/groups/ PUT /my_index/_mapping/groups {"properties": {"names": {"type": "string","position_increment_gap": 100}} }position_increment_gap 設置告訴 Elasticsearch 應該為數(shù)組中每個新元素增加當前詞條 position 的指定值。 所以現(xiàn)在當我們再索引 names 數(shù)組時,會產(chǎn)生如下的結(jié)果:
* Position 1: john
* Position 2: abraham
* Position 103: lincoln
* Position 104: smith
現(xiàn)在我們的短語查詢可能無法匹配該文檔因為 abraham 和 lincoln 之間的距離為 100 。 為了匹配這個文檔你必須添加值為 100 的 slop 。position_increment_gap默認是100.
另外,我們也可以在自定義分析器的時候設置該參數(shù)。
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
總結(jié)
以上是生活随笔為你收集整理的elasticsearch 关键词查询-实现like查询的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【学习笔记】支配树
- 下一篇: 转载: WebKit介绍及总结(一)