无论是新闻、内容、还是电商平台,联想输入已经成为搜索功能的标配,早已不是什么新鲜事物。我们随便打开一个搜索引擎或者是电商平台,当我们在输入框输入拼音或者文字时就会看到输入框下方弹出有意义的搜索建议,提示我们是不是想要输入“以下”内容,帮助我们补齐输入或是修正错误的输入,优化我们的搜索体验。
搜索引擎的另一个标配就是热门搜索推荐,让用户能够了解到当下大家最关心的新闻、商品或是八卦,在用户不知道想要搜索什么的时候,给用户一些选择,优化用户体验,实际上也是达到广告的效果。
当然,笔者不是产品经理,所以我们不分析联想输入与热门搜索推荐功能背后的意义,仅从技术角度介绍大家最感兴趣的技术问题,即如何为搜索添加联想输入搜索与热门搜索推荐功能,这也是笔者最近接到的一个需求。
本篇内容介绍如何基于ElasticSearch
为商品搜索添加联想输入与热门搜索推荐功能。
实现热门搜索推荐最简单的方式就是在用户点击搜索时记录用户输入的文本内容,然后为前端提供一个接口,用于统计所有用户输入的文本内容取出出现次数最多的前几条记录响应给前端。当然,最好加上搜索时间,统计时加上时间范围过滤,只关心最近一段时间的数据。
假设我们创建的商品搜索日记索引名称为search_log
。
Kibana
中调用接口PUT /search_log
{
"mappings":{
"properties":{
"keyword":{
"type":"keyword",
"ignore_above":256
},
"search_time":{
"type":"long"
}
}
},
"settings": {
"index": {
"number_of_shards": 5,
"number_of_replicas": 3
}
}
}
在search_log
索引中,我们为keyword
字段设置类型为keyword
类型。
Text
类型:查询时先分词再索引,支持模糊查询,不支持聚合;keyword
类型:查询时不进行分词,直接索引,支持模糊查询,支持聚合。Kibana
中调用接口GET /search_log/_search
{
"query": {
"bool": {
"filter": [
{
"range": {
"search_time": {
"gte": 1000000000000,
"lte": 10000000000000
}
}
}
]
}
},
"aggs": {
"my_name": {
"terms": {
"field": "keyword",
"size": 1
}
}
}
}
Java
代码实现,聚合搜索获取出现次数最多的前几条记录@Service
public class GoodsSearchService{
@Override
public List<String> popularSearch(int n) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.query(QueryBuilders.boolQuery()
.filter(QueryBuilders.rangeQuery("search_time")
.gte(System.currentTimeMillis() - (1000L * 31 * 24 * 60 * 60))));
searchSourceBuilder.aggregation(AggregationBuilders.terms("group_by_keyword")
.field("keyword").size(n));
try {
SearchRequest searchRequest = new SearchRequest("search_log");
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
Aggregations aggregations = searchResponse.getAggregations();
Terms trem = aggregations.get("group_by_keyword");
List<Terms.Bucket> buckets = (List<Terms.Bucket>) trem.getBuckets();
List<String> hotWords = new ArrayList<>();
for (Terms.Bucket bucket : buckets) {
String key = (String) bucket.getKey();
hotWords.add(key);
}
return hotWords;
} catch (IOException e) {
e.printStackTrace();
return Collections.emptyList();
}
}
}
笔者在做这个功能的时候也有过一个疑问:这个功能应该是放在前端实现呢,还是后端实现呢?
为解答这个问题,笔者去看了谷歌搜索、百度搜索以及淘宝搜索,发现这些搜索引擎都是通过请求后端接口实现的。
谷歌搜索:
百度搜索:
淘宝搜索:
很好理解,以淘宝搜索为例,因为前端没有商品,联想输入提示需要知道商品库里有没有商品名称以用户输入的字符串为前缀的商品,总不能给用户提示搜索不存在的商品,而且也还需要根据匹配成绩实现排序。
我们是基于ElasticSearch
提供的Completion Suggest
实现的联想输入提示功能。只需要在创建索引时为字段加上如下属性:
"fields":{
"suggest":{
"type":"completion",
"analyzer":"ik_max_word"
}
例如给商品索引的商品名称字段添加Suggest
,类型选择Completion
,则配置如下:
"goodsName":{
"search_analyzer":"ik_smart",
"analyzer":"ik_max_word",
"type":"text",
"fields":{
"suggest":{
"type":"completion",
"analyzer":"ik_max_word"
}
}
}
注意:如果索引已经存在了就没有那么简单了,ElasticSearch
不支持修改索引字段,所以只能通过reindex
修改索引字段信息。
PUT /新索引名称
{
"mappings": {
"properties": {
// 其它字段省略
"goodsName": {
"search_analyzer": "ik_smart",
"analyzer": "ik_max_word",
"type": "text",
"fields": {
"suggest": {
"type": "completion",
"analyzer": "ik_max_word"
}
}
}
}
},
"settings":{
"index":{
"number_of_shards":5,
"number_of_replicas":3
}
}
}
reindex
操作当数据量非常大时,reindex
耗时非常长,因此需要在reindex
请求url
加上wait_for_completion=false
参数让reindex
异步执行。
提示:我们测试环境商品库五千多万的数据,在reindex十几分钟后由于内存不足最终把ElasticSearch搞崩了,而且也只同步了九百多万条商品记录,因此reindex一定要非常小心操作,根据索引的文档数量和可用内存做好风险评估。
POST _reindex?wait_for_completion=false
{
"source": {
"index": "旧索引名称"
},
"dest": {
"index": "新索引名称"
}
}
等待两个索引的记录总数相同时再进行下一步操作,即等待以下两个请求返回相同的结果:
GET /新索引名称/_count
GET /旧索引名称/_count
DELETE /旧索引名称
POST _aliases
{
"actions": [
{
"add": {
"index": "新索引名称",
"alias": "旧索引名称"
}
}
]
}
Kibana
中调用接口GET /索引名称/_search
{
"size": 0,
"suggest": {
"goodsNameSuggest": {
"prefix":"BOBO",
"completion": {
"field": "goodsName.suggest",
"size":1
}
}
}
}
Java
代码实现@Service
public class GoodsSearchService{
@Override
public List<String> lenovoSearch(String inputStr) {
CompletionSuggestionBuilder suggestion = SuggestBuilders
.completionSuggestion("goodsName.suggest")
.prefix(inputStr)
.size(10);
SuggestBuilder suggestBuilder = new SuggestBuilder();
suggestBuilder.addSuggestion("mySuggest", suggestion);
SearchRequest searchRequest = new SearchRequest("商品索引名称");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.suggest(suggestBuilder);
searchSourceBuilder.size(0);
searchRequest.source(searchSourceBuilder);
try {
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
Suggest suggest = response.getSuggest();
// 没有任何数据
if (suggest == null) {
return Collections.emptyList();
}
Suggest suggestResult = response.getSuggest();
Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>>
suggestionResult = suggestResult.getSuggestion("mySuggest");
List<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> list
= suggestionResult.getEntries();
List<String> suggestList = new ArrayList<>();
if (list == null) {
return null;
} else {
for (Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option> e : list) {
for (Suggest.Suggestion.Entry.Option option : e) {
suggestList.add(option.getText().toString());
}
}
}
return suggestList;
} catch (IOException e) {
e.printStackTrace();
return Collections.emptyList();
}
}
}
热门搜索推荐功能相对简单,即便不使用ElasticSearch
,也可以选择其它的分析型数据库代替,比较难的是联想输入,并且对性能要求高,因为前端每输入一个字或字母都需要请求接口,靠数据库前缀匹配查询无法实现高性能。但看起来非常难实现的功能ElasticSearch
都帮我们解决了。
这里推荐一篇文章:《Google 搜索的即时自动补全功能究竟是如何“工作”的?》。