本章將和大家分享如何通過 Elasticsearch 實現(xiàn)自動補全查詢功能。
一、自動補全-安裝拼音分詞器
1、自動補全需求說明
當(dāng)用戶在搜索框輸入字符時,我們應(yīng)該提示出與該字符有關(guān)的搜索項,如圖:
2、使用拼音分詞
要實現(xiàn)根據(jù)字母做補全,就必須對文檔按照拼音分詞。在 GitHub 上恰好有 Elasticsearch 的拼音分詞插件。地址:https://github.com/infinilabs/analysis-pinyin
安裝方式與IK分詞器一樣,分三步:
1)解壓
2)上傳到 Elasticsearch 的 plugins 目錄下
3)重啟 Elasticsearch
4)測試?
首先從 GitHub 上下載?Elasticsearch 的拼音分詞插件,如下所示:
下載完成后,將其解壓出來,然后將解壓后的文件夾名稱重命名為 “py” ,最后把它復(fù)制到 Elasticsearch 的 plugins 目錄下,如下所示:
安裝完成后,需要重啟一下?Elasticsearch ,如下所示:
可以發(fā)現(xiàn)拼音分詞器插件安裝成功了。?
最后我們來測試一下:
# 測試拼音分詞 POST /_analyze { "text": "如家酒店還不錯", "analyzer": "pinyin" }
運行結(jié)果如下:
{ "tokens" : [ { "token" : "ru", "start_offset" : 0, "end_offset" : 0, "type" : "word", "position" : 0 }, { "token" : "rjjdhbc", "start_offset" : 0, "end_offset" : 0, "type" : "word", "position" : 0 }, { "token" : "jia", "start_offset" : 0, "end_offset" : 0, "type" : "word", "position" : 1 }, { "token" : "jiu", "start_offset" : 0, "end_offset" : 0, "type" : "word", "position" : 2 }, { "token" : "dian", "start_offset" : 0, "end_offset" : 0, "type" : "word", "position" : 3 }, { "token" : "hai", "start_offset" : 0, "end_offset" : 0, "type" : "word", "position" : 4 }, { "token" : "bu", "start_offset" : 0, "end_offset" : 0, "type" : "word", "position" : 5 }, { "token" : "cuo", "start_offset" : 0, "end_offset" : 0, "type" : "word", "position" : 6 } ] }
從該查詢結(jié)果可以看出拼音分詞器存在的一些問題:
1)第一個問題是拼音分詞器它不會分詞。
2)第二個問題是它把一句話里面的每一個字都形成了拼音,這對我們來說不僅沒什么用,而且還會占用空間。
3)第三個問題是拼音分詞結(jié)果中沒有漢字只剩下了拼音,而實際上我們用拼音搜索的情況是占少數(shù)的,大多數(shù)情況下其實我們是想通過中文去搜索的,所以說有拼音是錦上添花,但是不能把漢字給扔了,漢字也得保留。
這是我們拼音分詞器目前所面臨的幾個問題,因此我們就必須得對拼音分詞器做一些配置或者叫做自定義了,那么怎么樣才能實現(xiàn)自定義分詞器呢?
二、自動補全-自定義分詞器
Elasticsearch中分詞器(analyzer)的組成包含三部分:
- character filters:在tokenizer之前對文本進行處理。例如:刪除字符、替換字符。
- tokenizer:將文本按照一定的規(guī)則切割成詞條(term)。例如:keyword 就是不分詞、還有ik_smart 。
- tokenizer filter:對tokenizer輸出的詞條做進一步的處理。例如:大小寫轉(zhuǎn)換、同義詞處理、拼音處理等。
我們可以在創(chuàng)建索引庫時,通過settings來配置自定義的analyzer(分詞器):
PUT /test { "settings": { "analysis": { "analyzer": { //自定義分詞器 "my_analyzer": { //自定義分詞器的名稱 "tokenizer": "ik_max_word", "filter": "pinyin" } } } } }
上面這個只是解決了拼音分詞器分詞的問題,因此還需要對拼音分詞器做進一步的定制,如下所示:
# 自定義拼音分詞器 PUT /test { "settings": { "analysis": { "analyzer": { //自定義分詞器 "my_analyzer": { //自定義分詞器名稱 "tokenizer": "ik_max_word", "filter": "py" //過濾器名稱,可以是自定義的過濾器 } }, "filter": { //自定義tokenizer filter "py": { //自定義過濾器的名稱,可隨意取 "type": "pinyin", //過濾器類型,這里是pinyin "keep_full_pinyin": false, //修改可選參數(shù),具體可參考拼音分詞器GitHub官網(wǎng) "keep_joined_full_pinyin": true, "keep_original": true, "limit_first_letter_length": 16, "remove_duplicated_term": true, "none_chinese_pinyin_tokenize": false } } } }, "mappings": { "properties": { "name": { "type": "text", "analyzer": "my_analyzer" //使用自定義分詞器 } } } }
拼音分詞器更多可選參數(shù)可參考GitHub官網(wǎng):https://github.com/infinilabs/analysis-pinyin
test索引庫創(chuàng)建完成后,下面我們來測試下:
# 測試自定義分詞器 POST /test/_analyze { "text": [ "如家酒店還不錯" ], "analyzer": "my_analyzer" }
注意:在test索引庫中自定義的分詞器也只能在test索引庫中使用。
運行結(jié)果如下:
{ "tokens" : [ { "token" : "如家", "start_offset" : 0, "end_offset" : 2, "type" : "CN_WORD", "position" : 0 }, { "token" : "rujia", "start_offset" : 0, "end_offset" : 2, "type" : "CN_WORD", "position" : 0 }, { "token" : "rj", "start_offset" : 0, "end_offset" : 2, "type" : "CN_WORD", "position" : 0 }, { "token" : "酒店", "start_offset" : 2, "end_offset" : 4, "type" : "CN_WORD", "position" : 1 }, { "token" : "jiudian", "start_offset" : 2, "end_offset" : 4, "type" : "CN_WORD", "position" : 1 }, { "token" : "jd", "start_offset" : 2, "end_offset" : 4, "type" : "CN_WORD", "position" : 1 }, { "token" : "還不", "start_offset" : 4, "end_offset" : 6, "type" : "CN_WORD", "position" : 2 }, { "token" : "haibu", "start_offset" : 4, "end_offset" : 6, "type" : "CN_WORD", "position" : 2 }, { "token" : "hb", "start_offset" : 4, "end_offset" : 6, "type" : "CN_WORD", "position" : 2 }, { "token" : "不錯", "start_offset" : 5, "end_offset" : 7, "type" : "CN_WORD", "position" : 3 }, { "token" : "bucuo", "start_offset" : 5, "end_offset" : 7, "type" : "CN_WORD", "position" : 3 }, { "token" : "bc", "start_offset" : 5, "end_offset" : 7, "type" : "CN_WORD", "position" : 3 } ] }
可以看出,搜索結(jié)果中既有漢字、又有拼音、還有分詞,這完全符合我們的預(yù)期。
但是需要特別注意的是:拼音分詞器適合在創(chuàng)建倒排索引的時候使用,不適合在搜索的時候使用。下面我們通過一個例子來說明:
往test索引庫中加入2條測試數(shù)據(jù),如下所示:
POST /test/_doc/1 { "id": 1, "name": "獅子" } POST /test/_doc/2 { "id": 2, "name": "虱子" }
接著我們搜索“掉入獅子籠咋辦”,如下所示:
GET /test/_search { "query": { "match": { "name": "掉入獅子籠咋辦" } } }
運行結(jié)果如下:
可以發(fā)現(xiàn),其實我們是想找“獅子”,但是它把同音字“虱子” 也搜索出來了。下面我們通過一張圖來了解一下這個過程,如下所示:
由此可見,拼音分詞器適合在創(chuàng)建倒排索引的時候使用,但不能在搜索的時候使用。
正確的做法是:字段在創(chuàng)建倒排索引時應(yīng)該用my_analyzer分詞器,而字段在搜索時應(yīng)該使用ik_smart分詞器。如下所示:
# 自定義拼音分詞器 PUT /test { "settings": { "analysis": { "analyzer": { //自定義分詞器 "my_analyzer": { //自定義分詞器名稱 "tokenizer": "ik_max_word", "filter": "py" //過濾器名稱,可以是自定義的過濾器 } }, "filter": { //自定義tokenizer filter "py": { //自定義過濾器的名稱,可隨意取 "type": "pinyin", //過濾器類型,這里是pinyin "keep_full_pinyin": false, //修改可選參數(shù),具體可參考拼音分詞器GitHub官網(wǎng) "keep_joined_full_pinyin": true, "keep_original": true, "limit_first_letter_length": 16, "remove_duplicated_term": true, "none_chinese_pinyin_tokenize": false } } } }, "mappings": { "properties": { "name": { "type": "text", "analyzer": "my_analyzer", //創(chuàng)建倒排索引時使用自定義分詞器 "search_analyzer": "ik_smart" //搜索時應(yīng)該使用ik_smart分詞器 } } } }
總結(jié):
1、如何使用拼音分詞器?
- 下載 pinyin 分詞器
- 解壓并放到 Elasticsearch 的 plugins 目錄
- 重啟即可
2、如何自定義分詞器?
- 創(chuàng)建索引庫時,在 settings 中配置,可以包含三部分
- character filter
- tokenizer
- filter
3、拼音分詞器注意事項?
- 為了避免搜索到同音字,搜索時不要使用拼音分詞器。
三、自動補全-DSL實現(xiàn)自動補全查詢
Elasticsearch 提供了 Completion Suggester 查詢來實現(xiàn)自動補全功能。官方文檔地址:https://www.elastic.co/guide/en/elasticsearch/reference/7.6/search-suggesters.html#completion-suggester
這個查詢會匹配以用戶輸入內(nèi)容開頭的詞條并返回。為了提高補全查詢的效率,對于文檔中字段的類型有一些約束:
- 參與補全查詢的字段必須是completion類型。
- 字段的內(nèi)容一般是用來補全的多個詞條形成的數(shù)組。
# 創(chuàng)建索引庫 PUT test { "mappings": { "properties": { "title": { "type": "completion" } } } }
# 示例數(shù)據(jù) POST test/_doc { "title": [ "Sony", "WH-1000XM3" ] } POST test/_doc { "title": [ "SK-II", "PITERA" ] } POST test/_doc { "title": [ "Nintendo", "switch" ] }
查詢語法如下:
# 自動補全查詢 GET /test/_search { "suggest": { "title_suggest": { //自動補全查詢的名稱(自定義的名稱) "text": "s", //搜索關(guān)鍵字 "completion": { "field": "title", //自動補全查詢的字段 "skip_duplicates": true, //跳過重復(fù)的 "size": 10 //獲取前10條結(jié)果 } } } }
查詢結(jié)果如下:
{ "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 0, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "suggest" : { "title_suggest" : [ { "text" : "s", "offset" : 0, "length" : 1, "options" : [ { "text" : "SK-II", "_index" : "test", "_type" : "_doc", "_id" : "2CJaPY4Bne6OHhy3cho1", "_score" : 1.0, "_source" : { "title" : [ "SK-II", "PITERA" ] } }, { "text" : "Sony", "_index" : "test", "_type" : "_doc", "_id" : "1yJaPY4Bne6OHhy3WBoZ", "_score" : 1.0, "_source" : { "title" : [ "Sony", "WH-1000XM3" ] } }, { "text" : "switch", "_index" : "test", "_type" : "_doc", "_id" : "2SJaPY4Bne6OHhy3fBod", "_score" : 1.0, "_source" : { "title" : [ "Nintendo", "switch" ] } } ] } ] } }
自動補全對字段的要求:
- 類型是completion類型
- 字段值是多詞條的數(shù)組
四、自動補全-酒店數(shù)據(jù)自動補全(案例)
案例:實現(xiàn)hotel索引庫的自動補全、拼音搜索功能
實現(xiàn)思路如下:
1、修改hotel索引庫結(jié)構(gòu),設(shè)置自定義拼音分詞器。
2、修改索引庫的name、all字段,使用自定義分詞器。
3、索引庫添加一個新字段suggestion,類型為completion類型,使用自定義的分詞器。
4、給HotelDoc類添加suggestion字段,內(nèi)容包含brand、business 。
5、重新導(dǎo)入數(shù)據(jù)到hotel索引庫
注意:name、all是可分詞的,自動補全的brand、business是不可分詞的,要使用不同的分詞器組合。
# 創(chuàng)建酒店數(shù)據(jù)索引庫 PUT /hotel { "settings": { "analysis": { "analyzer": { "text_anlyzer": { //自定義分詞器,在創(chuàng)建倒排索引時使用 "tokenizer": "ik_max_word", "filter": "py" //自定義過濾器py }, "completion_analyzer": { //自定義分詞器,用于實現(xiàn)自動補全 "tokenizer": "keyword", //不分詞 "filter": "py" //自定義過濾器py } }, "filter": { //自定義tokenizer filter "py": { //自定義過濾器的名稱,可隨意取 "type": "pinyin", "keep_full_pinyin": false, //可選參數(shù)配置,具體可參考拼音分詞器Github官網(wǎng) "keep_joined_full_pinyin": true, "keep_original": true, "limit_first_letter_length": 16, "remove_duplicated_term": true, "none_chinese_pinyin_tokenize": false } } } }, "mappings": { "properties": { "id": { "type": "keyword" //不分詞 }, "name": { "type": "text", //分詞 "analyzer": "text_anlyzer", //在創(chuàng)建倒排索引時使用自定義分詞器text_anlyzer "search_analyzer": "ik_smart", //在搜索時使用ik_smart "copy_to": "all" //拷貝到all字段 }, "address": { "type": "keyword", "index": false //不創(chuàng)建倒排索引,不參與搜索 }, "price": { "type": "integer" }, "score": { "type": "integer" }, "brand": { "type": "keyword", "copy_to": "all" //拷貝到all字段 }, "city": { "type": "keyword" }, "starName": { "type": "keyword" }, "business": { "type": "keyword", "copy_to": "all" //拷貝到all字段 }, "location": { "type": "geo_point" //geo_point地理坐標(biāo)類型 }, "pic": { "type": "keyword", "index": false //不創(chuàng)建倒排索引,不參與搜索 }, "all": { //該字段主要用于搜索,沒有實際意義,且在搜索結(jié)果的原始文檔中你是看不到該字段的 "type": "text", "analyzer": "text_anlyzer", //在創(chuàng)建倒排索引時使用自定義分詞器text_anlyzer "search_analyzer": "ik_smart" //在搜索時使用ik_smart }, "suggestion": { //自動補全搜索字段 "type": "completion", //completion為自動補全類型 "analyzer": "completion_analyzer" //自動補全使用自定義分詞器completion_analyzer } } } }
由于hotel索引庫發(fā)生了變更,因此我們需要重新初始化一下ES的數(shù)據(jù),此處我采用了.net代碼實現(xiàn)了將酒店數(shù)據(jù)批量導(dǎo)入到ES中,關(guān)鍵代碼如下所示:
Hotel類(酒店數(shù)據(jù)):


using SqlSugar; namespace Demo.Domain.Entities { /// <summary> /// 酒店數(shù)據(jù) /// </summary> [SugarTable("tb_hotel")] //指定數(shù)據(jù)庫表名 public class Hotel { /// <summary> /// 酒店id /// </summary> [SugarColumn(IsPrimaryKey = true)] //數(shù)據(jù)庫是主鍵需要加上IsPrimaryKey public long id { get; set; } /// <summary> /// 酒店名稱 /// </summary> public string name { get; set; } /// <summary> /// 酒店地址 /// </summary> public string address { get; set; } /// <summary> /// 酒店價格 /// </summary> public int price { get; set; } /// <summary> /// 酒店評分 /// </summary> public int score { get; set; } /// <summary> /// 酒店品牌 /// </summary> public string brand { get; set; } /// <summary> /// 所在城市 /// </summary> public string city { get; set; } /// <summary> /// 酒店星級 /// </summary> [SugarColumn(ColumnName = "star_name")] //指定數(shù)據(jù)庫表字段 public string starName { get; set; } /// <summary> /// 商圈 /// </summary> public string business { get; set; } /// <summary> /// 緯度 /// </summary> public string latitude { get; set; } /// <summary> /// 經(jīng)度 /// </summary> public string longitude { get; set; } /// <summary> /// 酒店圖片 /// </summary> public string pic { get; set; } } }
HotelDoc類(酒店數(shù)據(jù)對應(yīng)的ES文檔):


using System; namespace Demo.Domain.Docs { /// <summary> /// 酒店數(shù)據(jù)對應(yīng)的ES文檔 /// </summary> public class HotelDoc { /// <summary> /// 酒店id /// </summary> public long id { get; set; } /// <summary> /// 酒店名稱 /// </summary> public string name { get; set; } /// <summary> /// 酒店地址 /// </summary> public string address { get; set; } /// <summary> /// 酒店價格 /// </summary> public int price { get; set; } /// <summary> /// 酒店評分 /// </summary> public int score { get; set; } /// <summary> /// 酒店品牌 /// </summary> public string brand { get; set; } /// <summary> /// 所在城市 /// </summary> public string city { get; set; } /// <summary> /// 酒店星級 /// </summary> public string starName { get; set; } /// <summary> /// 商圈 /// </summary> public string business { get; set; } /// <summary> /// 緯度 /// </summary> //public string latitude { get; set; } /// <summary> /// 經(jīng)度 /// </summary> //public string longitude { get; set; } /// <summary> /// 地理坐標(biāo)字段(將經(jīng)度和緯度字段合并成一個地理坐標(biāo)字段) /// 將經(jīng)度和緯度的字段值用英文逗號拼在一起,例如:"40.048969, 116.619566" /// </summary> public string location { get; set; } /// <summary> /// 酒店圖片 /// </summary> public string pic { get; set; } /// <summary> /// 自動補全搜索字段 /// </summary> public List<string> suggestion { get; set; } } }
Hotel類 和?HotelDoc類 二者的映射關(guān)系:


using AutoMapper; using Demo.Domain.Docs; using Demo.Domain.Entities; namespace Demo.Domain.AutoMapperConfigs { public class MyProfile : Profile { public MyProfile() { // 配置 mapping 規(guī)則 CreateMap<Hotel, HotelDoc>() .AfterMap((tbl, doc) => { #region 地理坐標(biāo)字段處理 if (!string.IsNullOrEmpty(tbl.latitude) && !string.IsNullOrEmpty(tbl.longitude)) { //將經(jīng)度和緯度的字段值用英文逗號拼在一起,例如:"40.048969, 116.619566" doc.location = string.Format(@"{0}, {1}", tbl.latitude, tbl.longitude); } #endregion #region 自動補全搜索字段處理 var suggestionList = new List<string>(); if (!string.IsNullOrEmpty(tbl.brand)) { //品牌 suggestionList.Add(tbl.brand); } if (!string.IsNullOrEmpty(tbl.business)) { //商圈 if (tbl.business.Contains("/")) { suggestionList.AddRange(tbl.business.Split('/')); } else { suggestionList.Add(tbl.business); } } doc.suggestion = suggestionList; #endregion }); } } }
將酒店數(shù)據(jù)批量插入到ES中:


using Microsoft.AspNetCore.Mvc; using AutoMapper; using Demo.Domain.Docs; using Demo.Domain.Entities; using Demo.Infrastructure.Repositories; using TianYaSharpCore.Elasticsearch; namespace Demo.MVC.Controllers { public class HomeController : Controller { private readonly HotelRepository _hotelRepository; private readonly IElasticClientProvider _elasticClientProvider; private readonly IMapper _mapper; public HomeController(HotelRepository hotelRepository, IElasticClientProvider elasticClientProvider, IMapper mapper) { _hotelRepository = hotelRepository; _elasticClientProvider = elasticClientProvider; _mapper = mapper; } public async Task<IActionResult> Index() { //從數(shù)據(jù)庫中查出所有的酒店數(shù)據(jù) var hotelList = await _hotelRepository._sqlSugarClient.Queryable<Hotel>().ToListAsync(); //實體轉(zhuǎn)換 var hotelDocList = _mapper.Map<List<HotelDoc>>(hotelList); //使用Nest將酒店數(shù)據(jù)批量插入到ES中 var asyncBulkIndexResponse = await _elasticClientProvider.ElasticLinqClient.BulkAsync(bulk => bulk .Index("hotel") .IndexMany(hotelDocList) ); return View(); } } }
ES數(shù)據(jù)初始化完成后,最后我們再去試一下酒店數(shù)據(jù)的自動補全查詢,如下所示:
# 自動補全查詢 GET /hotel/_search { "suggest": { "mySuggestion": { "text": "sd", "completion": { "field": "suggestion", "skip_duplicates": true, "size": 10 } } } }
運行結(jié)果如下所示:
{ "took" : 1, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 0, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "suggest" : { "mySuggestion" : [ { "text" : "sd", "offset" : 0, "length" : 2, "options" : [ { "text" : "上地產(chǎn)業(yè)園", "_index" : "hotel", "_type" : "_doc", "_id" : "2359697", "_score" : 1.0, "_source" : { "id" : 2359697, "name" : "如家酒店(北京上地安寧莊東路店)", "address" : "清河小營安寧莊東路18號20號樓", "price" : 420, "score" : 46, "brand" : "如家", "city" : "北京", "starName" : "二鉆", "business" : "上地產(chǎn)業(yè)園/西三旗", "location" : "40.041322, 116.333316", "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/2wj2f8mo9WZQCmzm51cwkZ9zvyp8_w200_h200_c1_t0.jpg", "suggestion" : [ "如家", "上地產(chǎn)業(yè)園", "西三旗" ] } }, { "text" : "首都機場", "_index" : "hotel", "_type" : "_doc", "_id" : "395702", "_score" : 1.0, "_source" : { "id" : 395702, "name" : "北京首都機場希爾頓酒店", "address" : "首都機場3號航站樓三經(jīng)路1號", "price" : 222, "score" : 46, "brand" : "希爾頓", "city" : "北京", "starName" : "五鉆", "business" : "首都機場/新國展地區(qū)", "location" : "40.048969, 116.619566", "pic" : "https://m.tuniucdn.com/fb2/t1/G6/M00/52/10/Cii-U13ePtuIMRSjAAFZ58NGQrMAAGKMgADZ1QAAVn_167_w200_h200_c1_t0.jpg", "suggestion" : [ "希爾頓", "首都機場", "新國展地區(qū)" ] } } ] } ] } }
至此本文就全部介紹完了,如果覺得對您有所啟發(fā)請記得點個贊哦!??!
?
Demo源碼:
鏈接:https://pan.baidu.com/s/1HohQqo1Mnycij7la07zZGw 提取碼:q807
此文由博主精心撰寫轉(zhuǎn)載請保留此原文鏈接:https://www.cnblogs.com/xyh9039/p/18063462文章來源:http://www.zghlxwxcb.cn/news/detail-840916.html
版權(quán)聲明:如有雷同純屬巧合,如有侵權(quán)請及時聯(lián)系本人修改,謝謝?。?!文章來源地址http://www.zghlxwxcb.cn/news/detail-840916.html
到了這里,關(guān)于Elasticsearch 系列(四)- DSL實現(xiàn)自動補全查詢的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!