一直覺得博客缺點東西,最近還是發現了,當博客慢慢多起來的時候想要找一篇之前寫的博客很是麻煩,於是作為後端開發的樓主覺得自己動手豐衣足食,也就有了這次博客全文檢索功能Elasticsearch實戰,這裡還要感謝一下‘輝哥’贊助的一臺伺服器。 <! more 全文檢索工具選型 眾所周知,支持全文檢索的工 ...
一直覺得博客缺點東西,最近還是發現了,當博客慢慢多起來的時候想要找一篇之前寫的博客很是麻煩,於是作為後端開發的樓主覺得自己動手豐衣足食,也就有了這次博客全文檢索功能Elasticsearch實戰,這裡還要感謝一下‘輝哥’贊助的一臺伺服器。
全文檢索工具選型
眾所周知,支持全文檢索的工具有很多,像 Lucene,solr, Elasticsearch 等,相比於其他的工具,顯然 Elasticsearch 社區更加活躍,遇到問題相對來說也比較好解決,另外 Elasticsearch 提供的restful介面操作起來還是比較方便的,這也是樓主選擇 Elasticsearch 的重要原因,當然 Elasticsearch 占據的記憶體相對來說比較大一點,樓主2G的雲伺服器跑起來也是捉襟見肘。
數據遷移,從 MySQL 到 Elasticsearch
這個功能相對來說比較簡單,就是定時從 MySQL 更新數據到 Elasticsearch 中,本來樓主打算自己寫一個數據遷移的工具,但是想起之前樓主做數據遷移時用到的DataX很是不錯,看了寫官方文檔還是支持的,但是樓主硬是沒有跑起來,原因就是樓主2G記憶體的雲伺服器不夠使啊,DataX光是跑起來就要1G多的記憶體,所以樓主只能另謀它法。對DataX感興趣的小伙伴可以看看樓主的另一篇文章阿裡離線數據同步工具 DataX 踩坑記錄。
說起可以省記憶體的語言,小伙伴可能會想到最近比較火的golang,沒錯樓主也想到了。最後樓主使用的就是一個叫go-mysql-elasticsearch的工具,就是使用golang實現的從 MySQL 將數據遷移到 Elasticsearch 的工具。具體搭建過程樓主不在這裡細說,感興趣的小伙伴請移步go-mysql-elasticsearch,另外 Elasticsearch 環境的搭建,需要註意的就是安裝 Elasticsearch 的機器記憶體應該大於或者等於2G,否則可能會出現起不起來的情況,樓主也不在這裡贅述了,比較簡單,請小伙伴們自行google。
另外需要註意的是,在使用 go-mysql-elasticsearch 的時候應該開啟mysql的binlog功能,go-mysql-elasticsearch的實現同步數據的思想就是將自己作為MySQL的一個slave掛載在MySQL上,這樣就可以很輕鬆的將數據實時同步到 Elasticsearch 中,在啟動 go-mysql-elasticsearch 的機器上最少應該有MySQL client工具,否則會啟動報錯。樓主的建議是根MySQL部署在同一臺機器上,因為golang耗費記憶體極少,並不會有太大影響。下麵給出樓主同步數據時 go-mysql-elasticsearch 的配置文件:
# MySQL address, user and password
# user must have replication privilege in MySQL.
my_addr = "127.0.0.1:3306"
my_user = "root"
my_pass = "******"
my_charset = "utf8"
# Set true when elasticsearch use https
#es_https = false
# Elasticsearch address
es_addr = "127.0.0.1:9200"
# Elasticsearch user and password, maybe set by shield, nginx, or x-pack
es_user = ""
es_pass = ""
# Path to store data, like master.info, if not set or empty,
# we must use this to support breakpoint resume syncing.
# TODO: support other storage, like etcd.
data_dir = "./var"
# Inner Http status address
stat_addr = "127.0.0.1:12800"
# pseudo server id like a slave
server_id = 1001
# mysql or mariadb
flavor = "mysql"
# mysqldump execution path
# if not set or empty, ignore mysqldump.
mysqldump = "mysqldump"
# if we have no privilege to use mysqldump with --master-data,
# we must skip it.
#skip_master_data = false
# minimal items to be inserted in one bulk
bulk_size = 128
# force flush the pending requests if we don't have enough items >= bulk_size
flush_bulk_time = "200ms"
# Ignore table without primary key
skip_no_pk_table = false
# MySQL data source
[[source]]
schema = "billboard-blog"
# Only below tables will be synced into Elasticsearch.
tables = ["content"]
# Below is for special rule mapping
[[rule]]
schema = "billboard-blog"
table = "content"
index = "contentindex"
type = "content"
[rule.field]
title="title"
blog_desc="blog_desc"
content="content"
# Filter rule
[[rule]]
schema = "billboard-blog"
table = "content"
index = "contentindex"
type = "content"
# Only sync following columns
filter = ["title", "blog_desc", "content"]
# id rule
[[rule]]
schema = "billboard-blog"
table = "content"
index = "contentindex"
type = "content"
id = ["id"]
實現全文檢索功能的服務
要想實現全文檢索的功能並對外提供服務,web服務必不可少,樓主使用Spring Boot搭建web服務,對Spring Boot感興趣的小伙伴也可以看一下樓主的另一篇文章,使用Spring Boot實現博客統計服務。好了廢話不多說了,請看代碼
介面實現代碼,代碼比較簡單就是接收參數,調用service代碼
@ApiOperation(value="全文檢索介面", notes="")
@ApiImplicitParam(name = "searchParam", value = "博客搜索條件(作者,描述,內容,標題)", required = true, dataType = "String")
@RequestMapping(value = "/get_content_list_from_es", method = RequestMethod.GET)
public ResultCode<List<ContentsWithBLOBs>> getContentListFromEs(String searchParam) {
ResultCode<List<ContentsWithBLOBs>> resultCode = new ResultCode();
try {
LOGGER.info(">>>>>> method getContentListFromEs request params : {},{},{}",searchParam);
resultCode = contentService.getContentListFromEs(searchParam);
LOGGER.info(">>>>>> method getContentListFromEs return value : {}",JSON.toJSONString(resultCode));
} catch (Exception e) {
e.printStackTrace();
resultCode.setCode(Messages.API_ERROR_CODE);
resultCode.setMsg(Messages.API_ERROR_MSG);
}
return resultCode;
}
service代碼實現,這裡代碼主要功能就是調用es的工具類,對博客描述,作者,博客標題,博客內容進行全文檢索。
@Override
public ResultCode<List<ContentsWithBLOBs>> getContentListFromEs(String searchParam) {
ResultCode resultCode = new ResultCode();
// 校驗參數,參數不能為空
if (StringUtils.isBlank(searchParam)) {
LOGGER.info(">>>>>> params not be null");
resultCode.setMsg(Messages.INPUT_ERROR_MSG);
resultCode.setCode(Messages.INPUT_ERROR_CODE);
return resultCode;
}
String matchStr = "blog_desc=" + searchParam;
List<Map<String, Object>> result = ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr);
matchStr = "author=" + searchParam;
result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr));
matchStr = "title=" + searchParam;
result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr));
matchStr = "content=" + searchParam;
result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr));
List<ContentsWithBLOBs> data = JSON.parseArray(JSON.toJSONString(result),ContentsWithBLOBs.class);
LOGGER.info("es return data : {}",JSON.toJSONString(result));
resultCode.setData(data);
return resultCode;
}
樓主用到的es的工具類代碼實現,就是使用es的java客戶端對es進行檢索。
/**
* 使用分詞查詢
*
* @param index 索引名稱
* @param type 類型名稱,可傳入多個type逗號分隔
* @param fields 需要顯示的欄位,逗號分隔(預設為全部欄位)
* @param matchPhrase true 使用,短語精準匹配
* @param matchStr 過濾條件(xxx=111,aaa=222)
* @return
*/
public static List<Map<String, Object>> searchListData(String index, String type, String fields, boolean matchPhrase, String matchStr) {
return searchListData(index, type, 0, 0, null, fields, null, matchPhrase, null, matchStr);
}
/**
* 使用分詞查詢
*
* @param index 索引名稱
* @param type 類型名稱,可傳入多個type逗號分隔
* @param startTime 開始時間
* @param endTime 結束時間
* @param size 文檔大小限制
* @param fields 需要顯示的欄位,逗號分隔(預設為全部欄位)
* @param sortField 排序欄位
* @param matchPhrase true 使用,短語精準匹配
* @param highlightField 高亮欄位
* @param matchStr 過濾條件(xxx=111,aaa=222)
* @return
*/
public static List<Map<String, Object>> searchListData(String index, String type, long startTime, long endTime, Integer size, String fields, String sortField, boolean matchPhrase, String highlightField, String matchStr) {
SearchRequestBuilder searchRequestBuilder = client.prepareSearch(index);
if (StringUtils.isNotEmpty(type)) {
searchRequestBuilder.setTypes(type.split(","));
}
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (startTime > 0 && endTime > 0) {
boolQuery.must(QueryBuilders.rangeQuery("processTime")
.format("epoch_millis")
.from(startTime)
.to(endTime)
.includeLower(true)
.includeUpper(true));
}
//搜索的的欄位
if (StringUtils.isNotEmpty(matchStr)) {
for (String s : matchStr.split(",")) {
String[] ss = s.split("=");
if (ss.length > 1) {
if (matchPhrase == Boolean.TRUE) {
boolQuery.must(QueryBuilders.matchPhraseQuery(s.split("=")[0], s.split("=")[1]));
} else {
boolQuery.must(QueryBuilders.matchQuery(s.split("=")[0], s.split("=")[1]));
}
}
}
}
// 高亮(xxx=111,aaa=222)
if (StringUtils.isNotEmpty(highlightField)) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
//highlightBuilder.preTags("<span style='color:red' >");//設置首碼
//highlightBuilder.postTags("</span>");//設置尾碼
// 設置高亮欄位
highlightBuilder.field(highlightField);
searchRequestBuilder.highlighter(highlightBuilder);
}
searchRequestBuilder.setQuery(boolQuery);
if (StringUtils.isNotEmpty(fields)) {
searchRequestBuilder.setFetchSource(fields.split(","), null);
}
searchRequestBuilder.setFetchSource(true);
if (StringUtils.isNotEmpty(sortField)) {
searchRequestBuilder.addSort(sortField, SortOrder.DESC);
}
if (size != null && size > 0) {
searchRequestBuilder.setSize(size);
}
//列印的內容 可以在 Elasticsearch head 和 Kibana 上執行查詢
LOGGER.info("\n{}", searchRequestBuilder);
SearchResponse searchResponse = searchRequestBuilder.execute().actionGet();
long totalHits = searchResponse.getHits().totalHits;
long length = searchResponse.getHits().getHits().length;
LOGGER.info("共查詢到[{}]條數據,處理數據條數[{}]", totalHits, length);
if (searchResponse.status().getStatus() == 200) {
// 解析對象
return setSearchResponse(searchResponse, highlightField);
}
return null;
}
最後,樓主使用postman測試web服務,如下圖所示:
過程中遇到的坑
IK分詞器的設置
這裡需要註意的是,Elasticsearch的版本一定要與ik分詞器的版本對應,不對應的話 Elasticsearch 會報錯的。
$ ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-6.3.0.zip
接著,重新啟動 Elastic,就會自動載入這個新安裝的插件。
然後,新建一個 Index,指定需要分詞的欄位。這一步根據數據結構而異,下麵的命令只針對本文。基本上,凡是需要搜索的中文欄位,都要單獨設置一下。
$ curl -X PUT 'localhost:9200/contentindex' -H 'Content-Type: application/json' -d '
{
"mappings": {
"content": {
"properties": {
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"blog_desc": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"author": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
}
}
}
}
}'
上面代碼中,首先新建一個名稱為contentindex的 Index,裡面有一個名稱為content的 Type。content有好多個欄位,這裡只為其中四個欄位指定分詞,content, title, blog_desc,author 。
這四個欄位都是中文,而且類型都是文本(text),所以需要指定中文分詞器,不能使用預設的英文分詞器。
MySQL binlog的設置
因為樓主運行 go-mysql-elasticsearch 的時候使用的MySQL的客戶端跟要導出數據的MySQL server端的版本不一致導致報錯,最終在 go-mysql-elasticsearch 原作者的幫助下解決,所以一定要使用同版本的MySQL server 與client,因為不同版本的MySQL特性不一樣,也就導致了 go-mysql-elasticsearch 導出數據有略微的不同。
小結
整個過程相對來說比較簡單,當然樓主通過這個功能的實現,也對es有了一個相對的認識,學習了一項新的技能,可能有的小伙伴對樓主的整個工程的代碼比較感興趣,暫時先不能透露,等樓主完善好了一併貢獻出來。