共計 9454 個字符,預計需要花費 24 分鐘才能閱讀完成。
這期內容當中丸趣 TV 小編將會給大家帶來有關如何使用 elasticsearch 搭建自己的搜索系統,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
什么是 elasticsearch#
Elasticsearch 是一個開源的高度可擴展的全文搜索和分析引擎,擁有查詢近實時的超強性能。
大名鼎鼎的 Lucene 搜索引擎被廣泛用于搜索領域,但是操作復雜繁瑣,總是讓開發者敬而遠之。而 Elasticsearch 將 Lucene 作為其核心來實現所有索引和搜索的功能,通過簡單的 RESTful 語法來隱藏掉 Lucene 的復雜性,從而讓全文搜索變得簡單
ES 在 Lucene 基礎上,提供了一些分布式的實現:集群,分片,復制等。
搜索為什么不用 MySQL 而用 es#
我們本文案例是一個迷你商品搜索系統,為什么不考慮使用 MySQL 來實現搜索功能呢?原因如下:
MySQL 默認使用 innodb 引擎,底層采用 b + 樹的方式來實現,而 Es 底層使用倒排索引的方式實現,使用倒排索引支持各種維度的分詞,可以掌控不同粒度的搜索需求。(MYSQL8 版本也支持了全文檢索,使用倒排索引實現,有興趣可以去看看兩者的差別)
如果使用 MySQL 的 %key% 的模糊匹配來與 es 的搜索進行比較,在 8 萬數據量時他們的耗時已經達到 40:1 左右,毫無疑問在速度方面 es 完勝。
es 在大廠中的應用情況 #
es 運用最廣泛的是 elk 組合來對日志進行搜索分析
58 安全部門、京東訂單中心幾乎全采用 es 來完成相關信息的存儲與檢索
es 在 tob 的項目中也用于各種檢索與分析
在 c 端產品中,企業通常自己基于 Lucene 封裝自己的搜索系統,為了適配公司營銷戰略、推薦系統等會有更多定制化的搜索需求
es 客戶端選型 #spring-boot-starter-data-elasticsearch#
我相信你看到的網上各類公開課視頻或者小項目均推薦使用這款 springboot 整合過的 es 客戶端,但是我們要 say no!
此圖是引入的最新版本的依賴,我們可以看到它所使用的 es-high-client 也為 6.8.7,而 es7.x 版本都已經更新很久了,這里許多新特性都無法使用,所以版本滯后是他最大的問題。而且它的底層也是 highclient,我們操作 highclient 可以更靈活。我呆過的兩個公司均未采用此客戶端。
elasticsearch-rest-high-level-client#
這是官方推薦的客戶端,支持最新的 es,其實使用起來也很便利,因為是官方推薦所以在特性的操作上肯定優于前者。而且該客戶端與 TransportClient 不同,不存在并發瓶頸的問題,官方首推,必為精品!
搭建自己的迷你搜索系統 #
引入 es 相關依賴,除此之外需引入 springboot-web 依賴、jackson 依賴以及 lombok 依賴等。
Copy properties
es.version 7.3.2 /es.version
/properties
!-- high client--
dependency
groupId org.elasticsearch.client /groupId
artifactId elasticsearch-rest-high-level-client /artifactId
version ${es.version} /version
exclusions
exclusion
groupId org.elasticsearch.client /groupId
artifactId elasticsearch-rest-client /artifactId
/exclusion
exclusion
groupId org.elasticsearch /groupId
artifactId elasticsearch /artifactId
/exclusion
/exclusions
/dependency
dependency
groupId org.elasticsearch /groupId
artifactId elasticsearch /artifactId
version ${es.version} /version
/dependency
!--rest low client high client 以來低版本 client 所以需要引入 --
dependency
groupId org.elasticsearch.client /groupId
artifactId elasticsearch-rest-client /artifactId
version ${es.version} /version
/dependency
es 配置文件 es-config.properties
Copyes.host=localhost
es.port=9200
es.token=es-token
es.charset=UTF-8
es.scheme=http
es.client.connectTimeOut=5000
es.client.socketTimeout=15000
封裝 RestHighLevelClient
Copy@Configuration@PropertySource(classpath:es-config.properties)public class RestHighLevelClientConfig { @Value( ${es.host} ) private String host; @Value(${es.port} ) private int port; @Value(${es.scheme} ) private String scheme; @Value(${es.token} ) private String token; @Value(${es.charset} ) private String charSet; @Value(${es.client.connectTimeOut} ) private int connectTimeOut; @Value(${es.client.socketTimeout} ) private int socketTimeout; @Bean
public RestClientBuilder restClientBuilder() { RestClientBuilder restClientBuilder = RestClient.builder( new HttpHost(host, port, scheme)
);
Header[] defaultHeaders = new Header[]{ new BasicHeader( Accept , */*), new BasicHeader(Charset , charSet), // 設置 token 是為了安全 網關可以驗證 token 來決定是否發起請求 我們這里只做象征性配置
new BasicHeader(E_TOKEN , token)
};
restClientBuilder.setDefaultHeaders(defaultHeaders);
restClientBuilder.setFailureListener(new RestClient.FailureListener(){ @Override
public void onFailure(Node node) {
System.out.println( 監聽某個 es 節點失敗
}
});
restClientBuilder.setRequestConfigCallback(builder -
builder.setConnectTimeout(connectTimeOut).setSocketTimeout(socketTimeout)); return restClientBuilder;
} @Bean
public RestHighLevelClient restHighLevelClient(RestClientBuilder restClientBuilder) { return new RestHighLevelClient(restClientBuilder);
}
}
封裝 es 常用操作
es 搜索系統封裝源碼
Copy@Servicepublic class RestHighLevelClientService {
@Autowired
private RestHighLevelClient client; @Autowired
private ObjectMapper mapper; /**
* 創建索引
* @param indexName
* @param settings
* @param mapping
* @return
* @throws IOException
*/
public CreateIndexResponse createIndex(String indexName, String settings, String mapping) throws IOException { CreateIndexRequest request = new CreateIndexRequest(indexName); if (null != settings ! .equals(settings)) { request.settings(settings, XContentType.JSON);
} if (null != mapping ! .equals(mapping)) { request.mapping(mapping, XContentType.JSON);
} return client.indices().create(request, RequestOptions.DEFAULT);
} /**
* 判斷 index 是否存在
*/
public boolean indexExists(String indexName) throws IOException { GetIndexRequest request = new GetIndexRequest(indexName); return client.indices().exists(request, RequestOptions.DEFAULT);
}
/**
* 搜索
*/
public SearchResponse search(String field, String key, String rangeField, String
from, String to,String termField, String termVal,
String ... indexNames) throws IOException{ SearchRequest request = new SearchRequest(indexNames);
SearchSourceBuilder builder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.must(new MatchQueryBuilder(field, key)).must(new RangeQueryBuilder(rangeField).from(from).to(to)).must(new TermQueryBuilder(termField, termVal));
builder.query(boolQueryBuilder);
request.source(builder);
log.info([ 搜索語句為:{}] ,request.source().toString()); return client.search(request, RequestOptions.DEFAULT);
} /**
* 批量導入
* @param indexName
* @param isAutoId 使用自動 id 還是使用傳入對象的 id
* @param source
* @return
* @throws IOException
*/
public BulkResponse importAll(String indexName, boolean isAutoId, String source) throws IOException{ if (0 == source.length()){ //todo 拋出異常 導入數據為空
}
BulkRequest request = new BulkRequest();
JsonNode jsonNode = mapper.readTree(source); if (jsonNode.isArray()) { for (JsonNode node : jsonNode) { if (isAutoId) { request.add(new IndexRequest(indexName).source(node.asText(), XContentType.JSON));
} else { request.add(new IndexRequest(indexName)
.id(node.get( id).asText())
.source(node.asText(), XContentType.JSON));
}
}
} return client.bulk(request, RequestOptions.DEFAULT);
}
創建索引,這里的 settings 是設置索引是否設置復制節點、設置分片個數,mappings 就和數據庫中的表結構一樣,用來指定各個字段的類型,同時也可以設置字段是否分詞(我們這里使用 ik 中文分詞器)、采用什么分詞方式。
Copy @Test
public void createIdx() throws IOException { String settings = + {\n + \ number_of_shards\ : \ 2\ ,\n + \ number_of_replicas\ : \ 0\ \n + }
String mappings = + {\n + \ properties\ : {\n + \ itemId\ : {\n + \ type\ : \ keyword\ ,\n + \ ignore_above\ : 64\n + },\n + \ urlId\ : {\n + \ type\ : \ keyword\ ,\n + \ ignore_above\ : 64\n + },\n + \ sellAddress\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ courierFee\ : {\n + \ type\ : \ text\n + },\n + \ promotions\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ originalPrice\ : {\n + \ type\ : \ keyword\ ,\n + \ ignore_above\ : 64\n + },\n + \ startTime\ : {\n + \ type\ : \ date\ ,\n + \ format\ : \ yyyy-MM-dd HH:mm:ss\ \n + },\n + \ endTime\ : {\n + \ type\ : \ date\ ,\n + \ format\ : \ yyyy-MM-dd HH:mm:ss\ \n + },\n + \ title\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ serviceGuarantee\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ venue\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ currentPrice\ : {\n + \ type\ : \ keyword\ ,\n + \ ignore_above\ : 64\n + }\n + }\n + }
clientService.createIndex(idx_item , settings, mappings);
}
分詞技巧:
索引時最小分詞,搜索時最大分詞,例如 Java 知音 索引時分詞包含 Java、知音、音、知等,最小粒度分詞可以讓我們匹配更多的檢索需求,但是我們搜索時應該設置最大分詞,用“Java”和“知音”去匹配索引庫,得到的結果更貼近我們的目的,
對分詞字段同時也設置 keyword,便于后續排查錯誤時可以精確匹配搜索,快速定位。
我們向 es 導入十萬條淘寶雙 11 活動數據作為我們的樣本數據,數據結構如下所示
Copy{_id : https://detail.tmall.com/item.htm?id=538528948719\u0026skuId=3216546934499 , 賣家地址 : 上海 , 快遞費 : 運費: 0.00 元 , 優惠活動 : 滿 199 減 10, 滿 299 減 30, 滿 499 減 60, 可跨店 , 商品 ID : 538528948719 , 原價 : 2290.00 , 活動開始時間 : 2016-11-11 00:00:00 , 活動結束時間 : 2016-11-11 23:59:59 , 標題 : 【天貓海外直營】 ReFa CARAT RAY 黎琺 雙球滾輪波光美容儀 , 服務保障 : 正品保證; 贈運費險; 極速退款; 七天退換 , 會場 : 進口尖貨 , 現價 : 1950.00}
調用上面封裝的批量導入方法進行導入
Copy @Test
public void importAll() throws IOException { clientService.importAll( idx_item , true, itemService.getItemsJson());
}
我們調用封裝的搜索方法進行搜索,搜索產地為武漢、價格在 11-149 之間的相關酒產品,這與我們淘寶中設置篩選條件搜索商品操作一致。
Copy @Test
public void search() throws IOException {
SearchResponse search = clientService.search( title , 酒 , currentPrice , 11 , 149 , sellAddress , 武漢
SearchHits hits = search.getHits();
SearchHit[] hits1 = hits.getHits(); for (SearchHit documentFields : hits1) { System.out.println( documentFields.getSourceAsString());
}
}
我們得到以下搜索結果,其中_score 為某一項的得分,商品就是按照它來排序。
Copy { _index : idx_item , _type : _doc , _id : Rw3G7HEBDGgXwwHKFPCb , _score : 10.995819, _source : { itemId : 525033055044 , urlId : https://detail.tmall.com/item.htm?id=525033055044 skuId=def , sellAddress : 湖北武漢 , courierFee : 快遞: 0.00 , promotions : 滿 199 減 10, 滿 299 減 30, 滿 499 減 60, 可跨店 , originalPrice : 3768.00 , startTime : 2016-11-01 00:00:00 , endTime : 2016-11-11 23:59:59 , title : 酒嗨酒 西班牙原瓶原裝進口紅酒蒙德干紅葡萄酒 6 只裝整箱送酒具 , serviceGuarantee : 破損包退; 正品保證; 公益寶貝; 不支持 7 天退換; 極速退款 , venue : 食品主會場 , currentPrice : 151.00
}
}
擴展性思考 #
商品搜索權重擴展,我們可以利用多種收費方式智能為不同店家提供增加權重,增加曝光度適應自身的營銷策略。同時我們經常發現淘寶搜索前列的商品許多為我們之前查看過的商品,這是通過記錄用戶行為,跑模型等方式智能為這些商品增加權重。
分詞擴展,也許因為某些商品的特殊性,我們可以自定義擴展分詞字典,更精準、人性化的搜索。
高亮功能,es 提供 highlight 高亮功能,我們在淘寶上看到的商品展示中對搜索關鍵字高亮,就是通過這種方式來實現。
高亮使用方式
上述就是丸趣 TV 小編為大家分享的如何使用 elasticsearch 搭建自己的搜索系統了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注丸趣 TV 行業資訊頻道。