ES的使用
- 前言
- 作者使用的版本
- 作者需求
- 簡介
- ES簡略介紹
- ik分詞器簡介
- 使用
- es的直接簡單使用
- es的查詢
- es在java中使用
- 備注說明
前言
作者使用的版本
- es: 7.17.27
- spring-boot-starter-data-elasticsearch: 7.14.2
作者需求
作者接到一個業務需求,我們系統有份數據被用來查詢,進行搜索改造。我們的數據是可以分為兩層,第一層是物品,物品含有物品名和物品別名。第二層是規格型號,一個物品可以配置多個規格型號,同一物品不同規格型號有不同的價格。
原先的搜索只支持對物品名或別名的模糊搜索。將帶出該物品下所有的規格列表被用來選擇。比如說原來數據庫里有個物品叫做 蒙古大馬
,如果使用蒙古的大馬
進行搜索時搜索不到的,需要對這種搜索進行支持。
于是,作者選用了es作為文本搜索。作者只達到了可用的效果,并沒有深究es的文檔。底部會放置文檔鏈接。
如上所述,本文并不對es做詳細介紹。在上面說明中,作者已經碰到了問題,而es可以解決這個問題,所以本文只大概講述es為什么能夠解決我的問題
以及我如何使用es解決問題
。
簡介
ES簡略介紹
es為什么能夠解決我的問題
,我想要的搜索場景是,可以從用戶輸入的文本串中提取
出有用的匹配信息,然后到我的數據庫中對待匹配的信息
進行匹配。命中即認為該數據是用戶需要的,用戶輸入文本可能與數據庫中的某段信息的文字部分
是完全一致的,但是文字順序
是被打亂的。mysql數據庫支持like,所以可以將用戶輸入文字串分割成每個字,然后用or
去拼接查詢。這種查詢效率和使用方式可想而知有多么恐怖。但是es不需要這么麻煩的使用姿勢,es支持match
,可以對文本進行匹配搜索。為什么MySQL這么麻煩,而es看起來很方便呢?因為他們主要的應用場景就不同,所以底層的數據結構就不同。首先看看他們對數據的索引方式
:
MySQL的索引方式很簡單,一條記錄留存,如果指定了需要索引某個字段,就會將這個字段的值
和對應記錄的ID
拿出來,存儲到B+
樹中,二分搜索可以對有序數據進行快速查找,但是如果使用like '%x%'
;可以看到無法判斷要匹配文本的首字母了,也就是無法使用索引了,這樣數據量一旦上來,查詢效率會飛快下降。
es的索引方式中文一直叫做倒排索引
,什么叫倒排索引,在mysql中,索引的是這個字段的全文本,然后用全文本去匹配我們的輸入,如果匹配不到就錯了,換個數據繼續匹配。這個思路就是,我們需要所有數據的全文本,然后挨個匹配。可現在的場景是需要這么做,你只需要給我輸入的文本內有效部分
的有關數據即可。所以我們首先需要考慮將輸入信息進行分割
、提取
。然后用提取出來的每部分
數據參與匹配。那有個想法,如果我存的數據,也按照這種分割-提取
來提前建立一份索引呢?那就可以用提取到的部分輸入直接去匹配數據庫里提前存儲好的數據的索引。匹配到一個部分即認為命中
,匹配到多個部分認為相關度更高
,最終得分
也就越高。這種將文檔的文本打散進行索引再反過來查詢文檔的,被稱作倒排索引
。
ik分詞器簡介
既然有個數據庫可以支持倒排索引,那么接下來就看看怎么對文本分詞
,首先一個問題是為什么要分詞
,如果不分詞,我輸入一段話,每個字都是一個token
。那如果我認為有些字按照一定的順序組合起來有固定的含義,他們就成了詞,詞就意味著通用,我們在存儲和使用時都會按照這個順序。即他們也可以做token
。這樣一份文檔的token
數量就會減少,這樣可以顯著提升查詢的效率。
但是這也有個問題,比如上文的內蒙大馬
。如果我用了分詞器,ik的通用詞庫里有蒙古、大馬
,我現在用馬
去搜索,命中結果集為空,因為這段文本被索引為蒙古
、大馬
,你用馬
是匹配不到大馬
這個索引的。字是默認組詞使用的,如果有單字也是可以用使用的特殊情況,可以在詞典里添加這個單字,需要注意詞的順序,最短的詞放在上面,否則由于分詞器是不貪心
的,已經匹配了一個比較好的索引,就不去再考慮生成一個差的索引了。詞典添加后還要注意不要使用ik_smart
類型的分析器,下文會有說明。如果你認為你的場景每個字都需要考慮,那就簡單了,不要分詞
,以改兼賑、兩難自解
;
es本身不支持中文分詞,ik分詞器可以支持中文的分詞,所以需要ik分詞器。ik分詞有兩種type
:
ik_smart
:對文本進行粗粒度分詞,比如藍瘦香菇這個詞,我如果詞典里有藍瘦、香菇、藍瘦香菇。他的分詞結果藍瘦香菇;顯然,他的索引數量會更少,也會丟掉更多數據的查詢。
ik_max_word
,進行最粗粒度的分詞,會列舉所有的可能(但不包括已經被組詞的字
,所以單字的特殊情況可以將它作為詞來解決)。
每次更新詞庫都需要重啟,這太費勁了,還好ik支持熱更新詞庫。只需要在配置文件配置remote_ext_dict
即可。但是,更新詞典后只對之后的新數據有效,舊有數據需要重建索引
,
使用
es的直接簡單使用
- 創建索引
PUT /index_name
{"mappings": {"properties": {"ref_id": { "type": "long" },"ref_type": { "type": "integer" },"goods_name": { "type": "text","analyzer":"my_ik_analyzer","search_analyzer":"ik_max_word"},"param_value": { "type": "text","analyzer":"my_ik_analyzer","search_analyzer":"ik_max_word"}}}
}
- 刪除索引
DELETE /index_name
- 新增或更新文檔
POST /index_name/_doc/doc_id # 最后一位參數可以指定生成的文檔ID
{
"ref_id": 1,
"ref_type":1,
"goods_name": "狗;犬;野狗;導盲犬",
"param_value": "中華田園犬;美國狗;吃狗糧;奧利給"
}
- 分析測試
POST /index_name/_analyze?pretty
{
"analyzer": "ik_max_word",
"text":"內蒙大馬"
}
- 查詢
POST /bws_price_library/_search?pretty
{"query":
{"bool":{"must": [{ "multi_match": {"query":"大馬","fields":["goods_name^2","param_value"] #字段名后面的^n可以指定得分權重。}},{"term":{"ref_type":"101"}}]}
}}
es的查詢
es的查詢很負責,所以能覆蓋非常多的場景。作者上面涉及 準確查詢term
、匹配查詢match
,多字段匹配查詢multi_match
、布爾嵌套查詢bool
,只做了簡單的示例,很容易觸類旁通。具體的使用方式作者有時間會慢慢補充。
es在java中使用
- 保存
XXXXIndex index = new XXXXIndex();index.setId(BwsPriceConstant.SPECS_REF_TYPE_PART+"_"+id);index.setRefId(id);index.setRefType(BwsPriceConstant.SPECS_FEE_TYPE_PART);index.setGoodsName("name");index.setParamValue("value");IndexCoordinates indexCoordinates = elasticsearchRestTemplate.getIndexCoordinatesFor(XXXXIndex.class);elasticsearchRestTemplate.save(index,indexCoordinates);
- 刪除
elasticsearchRestTemplate.delete(id, XXXXIndex.class);
- 更新
IndexCoordinates indexCoordinates = elasticsearchRestTemplate.getIndexCoordinatesFor(XXXXIndex.class);Document document = Document.create();document.setId(index.getId());document.putIfAbsent("ref_id",index.getRefId());document.putIfAbsent("ref_type",index.getRefType());document.putIfAbsent("goods_name",index.getGoodsName());document.putIfAbsent("param_value",index.getParamValue());elasticsearchRestTemplate.update(UpdateQuery.builder(index.getId()).withDocument(document).build(),indexCoordinates);
- 查詢
Pageable pageable = PageRequest.of(reqVo.getCurrentPage()-1, reqVo.getPageSize());Map<String, Float> fields = new HashMap<>();fields.put("goods_name",3.0f);fields.put("param_value",1.0f);// 查詢實體使用構造器模式,multiMatchQuery.fields方法可以指定查詢權重,查看構造方法可以看到默認是都給了1的權重。這個權重也可以在構建索引的時候指定,不過查詢時指定的話會更靈活一些NativeSearchQuery build = new NativeSearchQueryBuilder().withPageable(pageable).withQuery(new BoolQueryBuilder().must(new TermQueryBuilder("ref_type", reqVo.getFeeType())).must(QueryBuilders.multiMatchQuery(reqVo.getName(), "goods_name", "param_value").fields(fields))).build();SearchHits<XXXXIndex> search = elasticsearchRestTemplate.search(build, XXXXIndex.class);List<SearchHit<XXXXIndex>> searchHits = search.getSearchHits();// SearchHit中的Content就是指定的類型對象
備注說明
在使用時建議做好數據的手動同步,以防萬一