elasticsearch搜素關鍵字自動補全顧名思義 在搜索框搜索時能有提示列表可供選擇。
最終效果如下:
該搜索優化功能是elasticsearch自帶的即suggest,suggest即存儲一個詞庫,每次搜索發送請求去詞庫中檢索,匹配到即返回。
接下來我們一步一步實現上述功能。
1.建立索引
我這預先準備了一個房屋信息的索引house
{
"settings": {
"number_of_replicas": 0
},
"mappings": {
"house": {
"dynamic": false,
"properties": {
"houseId": {
"type": "long"
},
"title": {
"type": "text",
"index": "analyzed",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"price": {
"type": "integer"
},
"area": {
"type": "integer"
},
"createTime": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
},
"lastUpdateTime": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
},
"cityEnName": {
"type": "keyword"
},
"regionEnName": {
"type": "keyword"
},
"direction": {
"type": "integer"
},
"distanceToSubway": {
"type": "integer"
},
"subwayLineName": {
"type": "keyword"
},
"subwayStationName": {
"type": "keyword"
},
"tags": {
"type": "text"
},
"street": {
"type": "keyword"
},
"district": {
"type": "keyword"
},
"description": {
"type": "text",
"index": "analyzed",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"layoutDesc": {
"type": "text",
"index": "analyzed",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"traffic": {
"type": "text",
"index": "analyzed",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"roundService": {
"type": "text",
"index": "analyzed",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"rentWay": {
"type": "integer"
},
"suggest": {
"type": "completion"
},
"room": {
"type": "integer"
}
}
}
}
}
注意關鍵字段suggest,type爲completion
2.向建好的索引中添加數據
主要注意suggest字段中如何添加數據
private boolean updateSuggest(HouseIndexTemplate indexTemplate) {
//將分詞字段加入AnalyzeRequestBuilder,通過ik_smart分詞後會生成多個詞組,然後將詞組加入suggest字段
AnalyzeRequestBuilder requestBuilder = new AnalyzeRequestBuilder(
this.esClient, AnalyzeAction.INSTANCE, INDEX_NAME, indexTemplate.getTitle(),
indexTemplate.getLayoutDesc(), indexTemplate.getRoundService(),
indexTemplate.getDescription(), indexTemplate.getSubwayLineName(),
indexTemplate.getSubwayStationName());
//採用ik_smart分詞
requestBuilder.setAnalyzer("ik_smart");
AnalyzeResponse response = requestBuilder.get();
List<AnalyzeResponse.AnalyzeToken> tokens = response.getTokens();
if (tokens == null) {
logger.warn("Can not analyze token for house: " + indexTemplate.getHouseId());
return false;
}
List<HouseSuggest> suggests = new ArrayList<>();
for (AnalyzeResponse.AnalyzeToken token : tokens) {
// 排序數字類型 & 小於2個字符的分詞結果
if ("<NUM>".equals(token.getType()) || token.getTerm().length() < 2) {
continue;
}
HouseSuggest suggest = new HouseSuggest();
suggest.setInput(token.getTerm());
suggests.add(suggest);
}
// 定製化小區自動補全(不需要分詞的字段手動額外加入)
HouseSuggest suggest = new HouseSuggest();
suggest.setInput(indexTemplate.getDistrict());
suggests.add(suggest);
indexTemplate.setSuggest(suggests);
return true;
}
如上代碼表示:
首先將title、layoutDesc、roundService、description、subwayLineName、subwayStationName通過ik_smart分詞後,將分詞後的字詞加入suggests 集合;
然後將不需要分詞的字段district手動加入suggests集合;
其中HouseIndexTemplate 只是一個索引對象,裏面封裝了索引所需字段,注意其中suggest爲 集合:
private List<HouseSuggest> suggest;
最後將HouseIndexTemplate對象寫入elasticsearch中即可。
IndexResponse response = this.esClient.prepareIndex(INDEX_NAME, INDEX_TYPE)
.setSource(objectMapper.writeValueAsBytes(indexTemplate), XContentType.JSON).get();
寫入後結構如下 :
3.開始做檢索
前端js調用檢索接口
$('#keyword-box').autocomplete({
minLength: 2, // 最小字符數,默認1
delay: 300, // 延遲加載300ms
source: function (request, response) { // 數據源
$.ajax({
url: '/rent/house/autocomplete?prefix=' + request.term,
success: function (res) {
if (res.code === 200) {
response(res.data);
}
}
});
},
select: function (event, ui) { // 選中事件
$('#keyword-box').text(ui.item.value);
window.location.href = locate_url(start, size);
}
});
後端接口和服務層:
controller:
/**
* 自動補全接口
*/
@GetMapping("rent/house/autocomplete")
@ResponseBody
public ApiResponse autocomplete(@RequestParam(value = "prefix") String prefix) {
if (prefix.isEmpty()) {
return ApiResponse.ofStatus(ApiResponse.Status.BAD_REQUEST);
}
ServiceResult<List<String>> result = this.searchService.suggest(prefix);
return ApiResponse.ofSuccess(result.getResult());
}
service:
@Override
public ServiceResult<List<String>> suggest(String prefix) {
CompletionSuggestionBuilder suggestion = SuggestBuilders.completionSuggestion("suggest").prefix(prefix).size(5);
SuggestBuilder suggestBuilder = new SuggestBuilder();
suggestBuilder.addSuggestion("autocomplete", suggestion);
SearchRequestBuilder requestBuilder = this.esClient.prepareSearch(INDEX_NAME)
.setTypes(INDEX_TYPE)
.suggest(suggestBuilder);
logger.debug(requestBuilder.toString());
SearchResponse response = requestBuilder.get();
Suggest suggest = response.getSuggest();
if (suggest == null) {
return ServiceResult.of(new ArrayList<>());
}
Suggest.Suggestion result = suggest.getSuggestion("autocomplete");
int maxSuggest = 0;
Set<String> suggestSet = new HashSet<>();
for (Object term : result.getEntries()) {
if (term instanceof CompletionSuggestion.Entry) {
CompletionSuggestion.Entry item = (CompletionSuggestion.Entry) term;
if (item.getOptions().isEmpty()) {
continue;
}
for (CompletionSuggestion.Entry.Option option : item.getOptions()) {
String tip = option.getText().string();
if (suggestSet.contains(tip)) {
continue;
}
suggestSet.add(tip);
maxSuggest++;
}
}
if (maxSuggest > 5) {
break;
}
}
List<String> suggests = Lists.newArrayList(suggestSet.toArray(new String[]{}));
return ServiceResult.of(suggests);
}