|
|
package com.yoho.search.service.servicenew.impl;
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.yoho.error.event.SearchEvent;
|
|
|
import com.yoho.search.base.utils.CharUtils;
|
|
|
import com.yoho.search.base.utils.EventReportEnum;
|
|
|
import com.yoho.search.base.utils.ISearchConstants;
|
|
|
import com.yoho.search.core.es.model.SearchParam;
|
|
|
import com.yoho.search.core.es.model.SearchResult;
|
|
|
import com.yoho.search.core.es.utils.IgnoreSomeException;
|
|
|
import com.yoho.search.service.service.SearchCacheService;
|
|
|
import com.yoho.search.service.service.SearchCommonService;
|
|
|
import com.yoho.search.service.servicenew.ISuggestService;
|
|
|
import com.yoho.search.service.utils.HttpServletRequestUtils;
|
|
|
import com.yoho.search.service.utils.SearchRequestParams;
|
|
|
import com.yoho.search.service.vo.SearchApiResult;
|
|
|
import com.yoho.search.service.vo.SuggestApiResult;
|
|
|
import org.apache.commons.collections.CollectionUtils;
|
|
|
import org.apache.commons.lang.StringUtils;
|
|
|
import org.elasticsearch.common.lucene.search.function.CombineFunction;
|
|
|
import org.elasticsearch.index.query.BoolQueryBuilder;
|
|
|
import org.elasticsearch.index.query.MatchQueryBuilder;
|
|
|
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
|
|
|
import org.elasticsearch.index.query.QueryBuilder;
|
|
|
import org.elasticsearch.index.query.QueryBuilders;
|
|
|
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
|
|
|
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
|
|
|
import org.elasticsearch.search.sort.SortBuilder;
|
|
|
import org.elasticsearch.search.sort.SortBuilders;
|
|
|
import org.elasticsearch.search.sort.SortOrder;
|
...
|
...
|
@@ -30,485 +32,271 @@ import org.springframework.context.ApplicationEventPublisher; |
|
|
import org.springframework.context.ApplicationEventPublisherAware;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.yoho.error.event.SearchEvent;
|
|
|
import com.yoho.search.base.utils.CharUtils;
|
|
|
import com.yoho.search.base.utils.EventReportEnum;
|
|
|
import com.yoho.search.base.utils.ISearchConstants;
|
|
|
import com.yoho.search.core.es.model.SearchParam;
|
|
|
import com.yoho.search.core.es.model.SearchResult;
|
|
|
import com.yoho.search.core.es.utils.IgnoreSomeException;
|
|
|
import com.yoho.search.service.service.SearchCacheService;
|
|
|
import com.yoho.search.service.service.SearchCommonService;
|
|
|
import com.yoho.search.service.service.SearchDynamicConfigService;
|
|
|
import com.yoho.search.service.service.SearchKeyWordService;
|
|
|
import com.yoho.search.service.servicenew.ISuggestService;
|
|
|
import com.yoho.search.service.utils.HttpServletRequestUtils;
|
|
|
import com.yoho.search.service.utils.SearchRequestParams;
|
|
|
import com.yoho.search.service.vo.SearchApiResult;
|
|
|
import com.yoho.search.service.vo.SuggestApiResult;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
|
|
|
@Service
|
|
|
public class SuggestServiceImpl implements ISuggestService, ApplicationEventPublisherAware {
|
|
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(SuggestServiceImpl.class);
|
|
|
|
|
|
private static final Logger CACHE_MATCH_REQUEST = LoggerFactory.getLogger("CACHE_MATCH_REQUEST");
|
|
|
|
|
|
private static final String SUGGEST_PARAM_APPTYPE = "app_type";
|
|
|
|
|
|
private static final String SUGGEST_PARAM_GLOBAL = "contain_global";
|
|
|
|
|
|
// 返回智能搜索词的数量
|
|
|
private static final int SMART_SUGGESTION_TERM_COUNT = 3;
|
|
|
|
|
|
// 返回智能搜索词限制关联商品数量限制
|
|
|
private static final int SMART_SUGGESTION_COUNT_LIMIT = 20;
|
|
|
|
|
|
private static final List<String> DEFAULT_SUGGEST_TIPS = Arrays.asList("潮流", "时尚", "休闲");
|
|
|
|
|
|
@Autowired
|
|
|
private SearchCommonService searchCommonService;
|
|
|
@Autowired
|
|
|
private SearchCacheService searchCacheService;
|
|
|
@Autowired
|
|
|
private SearchDynamicConfigService searchDynamicConfigService;
|
|
|
@Autowired
|
|
|
private SearchKeyWordService searchKeyWordService;
|
|
|
|
|
|
private ApplicationEventPublisher publisher;
|
|
|
|
|
|
@Override
|
|
|
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
|
|
this.publisher = applicationEventPublisher;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public SuggestApiResult suggest(Map<String, String> paramMap) {
|
|
|
// 已使用SuggestSearchHandler进行重构
|
|
|
try {
|
|
|
logger.info("[func=suggest][param={}][begin={}]", paramMap.toString(), System.currentTimeMillis());
|
|
|
|
|
|
// 1)关键参数验证
|
|
|
String keyword = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
|
|
|
if (StringUtils.isBlank(keyword)) {
|
|
|
return new SuggestApiResult().setCode(400).setMessage("关键字[query]参数为空!");
|
|
|
}
|
|
|
keyword = keyword.toLowerCase();
|
|
|
if (keyword.length() >= 30) {
|
|
|
return new SuggestApiResult().setCode(400).setMessage("关键字[query]参数非法!");
|
|
|
}
|
|
|
// 2)构建查询参数
|
|
|
SearchParam searchParam = buildSuggestSearchParam(paramMap);
|
|
|
|
|
|
// 3)先从缓存中获取
|
|
|
final String indexName = ISearchConstants.INDEX_NAME_SUGGEST;
|
|
|
JSONObject suggest = searchCacheService.getJSONObjectFromCache(indexName, searchParam);
|
|
|
if (suggest != null) {
|
|
|
CACHE_MATCH_REQUEST.info("match cache , url is :/suggest.json?" + HttpServletRequestUtils.genParamString(paramMap));
|
|
|
return new SuggestApiResult().setCode(200).setMessage("suggest List.").setSuggest(suggest);
|
|
|
}
|
|
|
|
|
|
// 4)进行ES检索
|
|
|
SearchResult searchResult = searchCommonService.doSearch(indexName, searchParam);
|
|
|
// 将searchResult转化为map返回--需要把aggregation转化为需要的结构
|
|
|
if (searchResult == null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
if (CollectionUtils.isEmpty(searchResult.getResultList())) {
|
|
|
// 5) suggest 纠错
|
|
|
SearchResult newSearchResult = getSpellCorrectResult(paramMap);
|
|
|
if (newSearchResult != null) {
|
|
|
searchResult = newSearchResult;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
suggest = new JSONObject();
|
|
|
List<Map<String, Object>> itemList = new ArrayList<Map<String, Object>>();
|
|
|
String countEsField = getCountField(paramMap);
|
|
|
for (Map<String, Object> map : searchResult.getResultList()) {
|
|
|
Map<String, Object> item = new HashMap<String, Object>();
|
|
|
item.put("item", map.get("keyword"));
|
|
|
item.put("frequency", map.get(countEsField));
|
|
|
item.put("type", map.get("type"));
|
|
|
itemList.add(item);
|
|
|
}
|
|
|
suggest.put("items", itemList);
|
|
|
searchCacheService.addJSONObjectToCache(indexName, searchParam, suggest);
|
|
|
return new SuggestApiResult().setCode(200).setMessage("suggest List.").setSuggest(suggest);
|
|
|
} catch (Exception e) {
|
|
|
logger.error(e.getMessage(), e);
|
|
|
publisher.publishEvent(new SearchEvent(EventReportEnum.SEARCHCONTROLLER_SUGGEST.getEventName(), EventReportEnum.SEARCHCONTROLLER_SUGGEST.getFunctionName(),
|
|
|
EventReportEnum.SEARCHCONTROLLER_SUGGEST.getMoudleName(), "exception", IgnoreSomeException.filterSomeException(e), null));
|
|
|
return new SuggestApiResult().setCode(500).setMessage(e.getMessage()).setSuggest(null);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private String getCountField(Map<String, String> paramMap) {
|
|
|
// suggest 支持PC、APP、BLK走不同的count字段 字段同SuggestIndexBO里保持一直
|
|
|
if (paramMap.containsKey(SUGGEST_PARAM_APPTYPE) && "1".equals(paramMap.get(SUGGEST_PARAM_APPTYPE))) {
|
|
|
return "countForBlk";
|
|
|
} else if (paramMap.containsKey(SUGGEST_PARAM_GLOBAL) && "Y".equals(paramMap.get(SUGGEST_PARAM_GLOBAL))) {
|
|
|
return "countForApp";
|
|
|
} else {
|
|
|
return "count";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private SearchResult getSpellCorrectResult(Map<String, String> paramMap) {
|
|
|
String keyword = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD).toLowerCase();
|
|
|
String newKeyword = getSpellCorrectKeyword(keyword);
|
|
|
if (StringUtils.isEmpty(newKeyword)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
logger.info("Switch the suggest keyword from [{}] to [{}].", keyword, newKeyword);
|
|
|
Map<String, String> newParamMap = new HashMap<>(paramMap.size());
|
|
|
newParamMap.putAll(paramMap);
|
|
|
newParamMap.put(SearchRequestParams.PARAM_SEARCH_KEYWORD, newKeyword);
|
|
|
|
|
|
SearchParam newSearchParam = buildSuggestSearchParam(newParamMap);
|
|
|
return searchCommonService.doSearch(ISearchConstants.INDEX_NAME_SUGGEST, newSearchParam);
|
|
|
} catch (Exception e) {
|
|
|
logger.error("Get new suggest result by keyword [" + newKeyword + "] failed!", e);
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private SearchParam buildSuggestSearchParam(Map<String, String> paramMap) {
|
|
|
// 2)构建查询参数
|
|
|
String keyword = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD).toLowerCase();
|
|
|
SearchParam searchParam = new SearchParam();
|
|
|
int size = SearchRequestParams.RESULT_SIZE_SUGGEST;
|
|
|
if (paramMap.containsKey(SearchRequestParams.PARAM_SEARCH_SIZE)) {
|
|
|
size = Integer.parseInt(paramMap.get(SearchRequestParams.PARAM_SEARCH_SIZE));
|
|
|
}
|
|
|
if (size > 10) {
|
|
|
size = 10;
|
|
|
}
|
|
|
|
|
|
QueryBuilder query = QueryBuilders.boolQuery().should(QueryBuilders.prefixQuery("keyword", keyword)).should(QueryBuilders.prefixQuery("keyword.keyword_pinyin", keyword))
|
|
|
.should(QueryBuilders.prefixQuery("keyword.keyword_jianpin", keyword));
|
|
|
searchParam.setQuery(query);
|
|
|
searchParam.setPage(1);
|
|
|
searchParam.setSize(size);
|
|
|
|
|
|
// count数量要>=1
|
|
|
String countEsField = getCountField(paramMap);
|
|
|
searchParam.setFiter(QueryBuilders.rangeQuery(countEsField).gte(2));
|
|
|
|
|
|
// 3)设置排序字段
|
|
|
List<SortBuilder> sortBuilders = new ArrayList<SortBuilder>();
|
|
|
sortBuilders.add(SortBuilders.fieldSort("weight").order(SortOrder.DESC));
|
|
|
sortBuilders.add(SortBuilders.fieldSort(countEsField).order(SortOrder.DESC));
|
|
|
searchParam.setSortBuilders(sortBuilders);
|
|
|
return searchParam;
|
|
|
}
|
|
|
|
|
|
private String getSpellCorrectKeyword(String keyword) {
|
|
|
try {
|
|
|
SearchParam suggestSearchParam = new SearchParam();
|
|
|
suggestSearchParam.setSize(0);
|
|
|
suggestSearchParam.setSuggestionBuilder(SuggestBuilders.termSuggestion("keyword_suggestion").text(keyword).field("keyword").size(1));
|
|
|
SearchResult searchResult = searchCommonService.doSearch(ISearchConstants.INDEX_NAME_SUGGEST, suggestSearchParam);
|
|
|
if (searchResult != null && searchResult.getSuggestionMap() != null && searchResult.getSuggestionMap().get("keyword_suggestion") != null) {
|
|
|
TermSuggestion suggestion = (TermSuggestion) searchResult.getSuggestionMap().get("keyword_suggestion");
|
|
|
List<TermSuggestion.Entry> entries = suggestion.getEntries();
|
|
|
if (CollectionUtils.isNotEmpty(entries)) {
|
|
|
List<TermSuggestion.Entry.Option> entryList = entries.get(0).getOptions();
|
|
|
if (CollectionUtils.isNotEmpty(entryList)) {
|
|
|
TermSuggestion.Entry.Option option = entryList.get(0);
|
|
|
return option.getText().string();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
} catch (Exception e) {
|
|
|
logger.error("Get suggestion by keyword [" + keyword + "] failed!", e);
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 根据query词获取term建议和phrase建议。 用于搜索结果数量太少或者无结果的时候给予用户的搜索建议。
|
|
|
*
|
|
|
* @param paramMap
|
|
|
* 商品列表搜索参数
|
|
|
* @return 包括term建议和phrase建议。
|
|
|
*/
|
|
|
public JSONObject suggestTips(Map<String, String> paramMap) {
|
|
|
// 1) 对query进行判断 为空时不处理
|
|
|
String queryWord = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
|
|
|
if (StringUtils.isEmpty(queryWord)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
// 2) 先调用suggest获取搜索提示词 只要能匹配到20%的term即可
|
|
|
JSONObject suggestTipResult = suggestTipsBySuggestIndex(paramMap);
|
|
|
if (suggestTipResult == null) {
|
|
|
// 2.1) 可能是ES发生异常 如果搜索不到相关是应该是集合为空而不是对象为null
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
List<String> resultTerms = (List<String>) suggestTipResult.get("terms_suggestion");
|
|
|
if (CollectionUtils.isNotEmpty(resultTerms)) {
|
|
|
// 2.2) 如果能直接从suggest获取到则直接返回
|
|
|
return suggestTipResult;
|
|
|
}
|
|
|
|
|
|
// 3) 通过conversion转换后再到suggest获取提示词
|
|
|
suggestTipResult = suggestTipsByConversionIndex(paramMap);
|
|
|
if (suggestTipResult != null && CollectionUtils.isNotEmpty((List<String>) suggestTipResult.get("terms_suggestion"))) {
|
|
|
return suggestTipResult;
|
|
|
}
|
|
|
|
|
|
return defaultSuggestTips();
|
|
|
} catch (Exception e) {
|
|
|
logger.error("[func=suggestTips]Get suggestion by keyword [" + queryWord + "] failed!", e);
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public JSONObject suggestTipsBySuggestIndex(Map<String, String> paramMap) {
|
|
|
String queryWord = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
|
|
|
long begin = System.currentTimeMillis();
|
|
|
logger.info("[func=suggestTipsBySuggestIndex][query={}][begin={}]", queryWord, begin);
|
|
|
// 1) 先对query进行分词
|
|
|
List<String> terms = searchKeyWordService.getAnalyzeTerms(queryWord, "ik_smart", true);
|
|
|
if (CollectionUtils.isEmpty(terms)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
Set<String> termSet = terms.stream().collect(Collectors.toSet());
|
|
|
logger.info("[func=suggestTipsBySuggestIndex][termSet={}]", termSet);
|
|
|
|
|
|
// 2) 根据terms搜索构造搜索请求
|
|
|
final String countField = getCountField(paramMap);
|
|
|
SearchParam searchParam = new SearchParam();
|
|
|
|
|
|
// 2.1) 对于suggest的multi-fields至少要有一个字段匹配到20% 匹配打分为常量1
|
|
|
MultiMatchQueryBuilder queryBuilder = QueryBuilders
|
|
|
.multiMatchQuery(queryWord.trim().toLowerCase(), "keyword", "keyword.keyword_ik", "keyword.keyword_pinyin", "keyword.keyword_jianpin", "keyword.keyword_lowercase")
|
|
|
.analyzer("ik_smart").type(MultiMatchQueryBuilder.Type.BEST_FIELDS).operator(MatchQueryBuilder.Operator.OR).minimumShouldMatch("20%");
|
|
|
|
|
|
FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(QueryBuilders.constantScoreQuery(queryBuilder));
|
|
|
for (String term : termSet) {
|
|
|
// 2.2) 对于完全匹配Term的加1分
|
|
|
functionScoreQueryBuilder.add(QueryBuilders.termQuery("standardKeyword", CharUtils.standardized(term)), ScoreFunctionBuilders.weightFactorFunction(1));
|
|
|
|
|
|
// 2.3) 对于匹配到一个Term的加2分
|
|
|
functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_ik", term), ScoreFunctionBuilders.weightFactorFunction(2));
|
|
|
}
|
|
|
|
|
|
functionScoreQueryBuilder.boostMode(CombineFunction.SUM);
|
|
|
searchParam.setQuery(functionScoreQueryBuilder);
|
|
|
searchParam.setPage(1);
|
|
|
searchParam.setSize(SMART_SUGGESTION_TERM_COUNT);
|
|
|
|
|
|
BoolQueryBuilder boolFilter = QueryBuilders.boolQuery();
|
|
|
boolFilter.must(QueryBuilders.rangeQuery(countField).gte(SMART_SUGGESTION_COUNT_LIMIT));
|
|
|
boolFilter.mustNot(QueryBuilders.termQuery("standardKeyword", CharUtils.standardized(queryWord)));
|
|
|
searchParam.setFiter(boolFilter);
|
|
|
|
|
|
// 2.4) 按照得分、权重、数量的规则降序排序
|
|
|
List<SortBuilder> sortBuilders = new ArrayList<>(3);
|
|
|
sortBuilders.add(SortBuilders.fieldSort("_score").order(SortOrder.DESC));
|
|
|
sortBuilders.add(SortBuilders.fieldSort("weight").order(SortOrder.DESC));
|
|
|
sortBuilders.add(SortBuilders.fieldSort(countField).order(SortOrder.DESC));
|
|
|
searchParam.setSortBuilders(sortBuilders);
|
|
|
|
|
|
// 3) 先从缓存中获取
|
|
|
final String indexName = ISearchConstants.INDEX_NAME_SUGGEST;
|
|
|
JSONObject suggestResult = searchCacheService.getJSONObjectFromCache(indexName, searchParam);
|
|
|
if (suggestResult != null) {
|
|
|
CACHE_MATCH_REQUEST.info("match cache for product list terms suggestion, keyword = {}.", queryWord);
|
|
|
return suggestResult;
|
|
|
}
|
|
|
|
|
|
// 4) 调用ES执行搜索
|
|
|
SearchResult searchResult = searchCommonService.doSearch(ISearchConstants.INDEX_NAME_SUGGEST, searchParam);
|
|
|
if (searchResult == null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
// 5) 加入缓存
|
|
|
suggestResult = new JSONObject();
|
|
|
List<String> resultTerms = searchResult.getResultList().stream().map(map -> (String) map.get("keyword")).collect(Collectors.toList());
|
|
|
suggestResult.put("terms_suggestion", resultTerms);
|
|
|
searchCacheService.addJSONObjectToCache(indexName, searchParam, suggestResult);
|
|
|
logger.info("[func=suggestTipsBySuggestIndex][query={}][cost={}]", queryWord, System.currentTimeMillis() - begin);
|
|
|
return suggestResult;
|
|
|
}
|
|
|
|
|
|
private JSONObject suggestTipsByConversionIndex(Map<String, String> paramMap) {
|
|
|
String queryWord = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD).toLowerCase();
|
|
|
long begin = System.currentTimeMillis();
|
|
|
logger.info("[func=suggestTipsByConversionIndex][query={}][begin={}]", queryWord, begin);
|
|
|
|
|
|
// 1) 判断是否支持从conversion获取推荐词
|
|
|
if (!searchDynamicConfigService.isSearchSuggestionFromConversionOpen()) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
// 2) 异步上报query关键词 用于统计和分析
|
|
|
searchKeyWordService.recordSuggestTip(queryWord);
|
|
|
|
|
|
// 3) 从conversion索引中获取转换后的关键词列表
|
|
|
String dest = getSuggestConversionDestBySource(queryWord);
|
|
|
if (StringUtils.isEmpty(dest)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
// 4) 查询suggest索引 过滤和排序
|
|
|
// 4.1) 根据查询条件获得不同的count字段
|
|
|
final String countFiled = getCountField(paramMap);
|
|
|
SearchParam searchParam = new SearchParam();
|
|
|
List<String> keywordsInDest = Arrays.stream(dest.split(",")).map(String::toLowerCase).map(String::trim).collect(Collectors.toList());
|
|
|
keywordsInDest.remove(queryWord);
|
|
|
|
|
|
// 4.2) 设置keyword列表
|
|
|
QueryBuilder queryBuilder = QueryBuilders.termsQuery("keyword.keyword_lowercase", keywordsInDest);
|
|
|
FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(queryBuilder);
|
|
|
|
|
|
// 4.3) 确保品类的关键词排在前面
|
|
|
functionScoreQueryBuilder.add(QueryBuilders.termQuery("type", 2), ScoreFunctionBuilders.weightFactorFunction(keywordsInDest.size() + 1));
|
|
|
|
|
|
// 4.4) 根据顺序从高到低加分
|
|
|
for (int index = 0; index < keywordsInDest.size(); index++) {
|
|
|
functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_lowercase", keywordsInDest.get(index)),
|
|
|
ScoreFunctionBuilders.weightFactorFunction(keywordsInDest.size() - index));
|
|
|
}
|
|
|
|
|
|
searchParam.setQuery(functionScoreQueryBuilder);
|
|
|
searchParam.setPage(1);
|
|
|
searchParam.setSize(SMART_SUGGESTION_TERM_COUNT);
|
|
|
|
|
|
// 4.5) 过滤关联的商品数量小于20的关键词
|
|
|
searchParam.setFiter(QueryBuilders.rangeQuery(countFiled).gte(SMART_SUGGESTION_COUNT_LIMIT));
|
|
|
|
|
|
// 5) 先从缓存中获取
|
|
|
final String indexName = ISearchConstants.INDEX_NAME_SUGGEST;
|
|
|
JSONObject jsonObject = searchCacheService.getJSONObjectFromCache(indexName, searchParam);
|
|
|
if (jsonObject != null) {
|
|
|
CACHE_MATCH_REQUEST.info("match cache for product list terms suggestion conversion, keyword = {}.", queryWord);
|
|
|
return jsonObject;
|
|
|
}
|
|
|
|
|
|
// 6) 调用ES获取结果
|
|
|
SearchResult searchResult = searchCommonService.doSearch(indexName, searchParam);
|
|
|
if (searchResult == null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
// 7) 将结果放入缓存
|
|
|
jsonObject = new JSONObject();
|
|
|
List<String> resultTerms = searchResult.getResultList().stream().map(map -> (String) map.get("keyword")).collect(Collectors.toList());
|
|
|
jsonObject.put("terms_suggestion", resultTerms);
|
|
|
searchCacheService.addJSONObjectToCache(indexName, searchParam, jsonObject);
|
|
|
logger.info("[func=suggestTipsByConversionIndex][query={}][cost={}]", queryWord, System.currentTimeMillis() - /**/begin);
|
|
|
return jsonObject;
|
|
|
}
|
|
|
|
|
|
private String getSuggestConversionDestBySource(String queryWord) {
|
|
|
long begin = System.currentTimeMillis();
|
|
|
logger.info("[func=getSuggestConversionDestBySource][query={}][begin={}]", queryWord, begin);
|
|
|
|
|
|
// 2) 根据terms搜索构造搜索请求
|
|
|
SearchParam searchParam = new SearchParam();
|
|
|
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("source", queryWord);
|
|
|
searchParam.setQuery(queryBuilder);
|
|
|
searchParam.setPage(1);
|
|
|
searchParam.setSize(1);
|
|
|
searchParam.setFiter(QueryBuilders.termQuery("status", 1));
|
|
|
|
|
|
// 3) 先从缓存中获取
|
|
|
final String indexName = ISearchConstants.INDEX_NAME_CONVERSION;
|
|
|
JSONObject suggestResult = searchCacheService.getJSONObjectFromCache(indexName, searchParam);
|
|
|
if (suggestResult != null) {
|
|
|
CACHE_MATCH_REQUEST.info("match cache for product list terms conversion, keyword = {}.", queryWord);
|
|
|
return suggestResult.getString("dest");
|
|
|
}
|
|
|
|
|
|
// 4) 调用ES执行搜索
|
|
|
SearchResult searchResult = searchCommonService.doSearch(indexName, searchParam);
|
|
|
if (searchResult == null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
String dest = "";
|
|
|
if (CollectionUtils.isNotEmpty(searchResult.getResultList())) {
|
|
|
// 5) 从conversion获取的keyword列表
|
|
|
dest = (String) searchResult.getResultList().get(0).get("dest");
|
|
|
logger.info("[func=getSuggestConversionDestBySource][source={}][dest={}]", searchResult.getResultList().get(0).get("source"), dest);
|
|
|
}
|
|
|
|
|
|
// 6) 加入缓存
|
|
|
suggestResult = new JSONObject();
|
|
|
suggestResult.put("dest", dest);
|
|
|
searchCacheService.addJSONObjectToCache(indexName, searchParam, suggestResult);
|
|
|
logger.info("[func=getSuggestConversionDestBySource][query={}][cost={}]", queryWord, System.currentTimeMillis() - begin);
|
|
|
return dest;
|
|
|
}
|
|
|
|
|
|
private JSONObject defaultSuggestTips() {
|
|
|
JSONObject defSuggestResult = new JSONObject();
|
|
|
defSuggestResult.put("terms_suggestion", DEFAULT_SUGGEST_TIPS);
|
|
|
return defSuggestResult;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 根据关键词和时间查询关键词转换关系
|
|
|
*
|
|
|
* @param paramMap
|
|
|
* 查询参数
|
|
|
* @return 满足查询要求的转换关系
|
|
|
*/
|
|
|
@Override
|
|
|
public SearchApiResult suggestConversion(Map<String, String> paramMap) {
|
|
|
try {
|
|
|
long begin = System.currentTimeMillis();
|
|
|
logger.info("[func=suggestConversion][param={}][begin={}]", paramMap, begin);
|
|
|
|
|
|
String queryWord = paramMap.get("query");
|
|
|
int updateTime = StringUtils.isBlank(paramMap.get("updateTime")) ? 0 : Integer.parseInt(paramMap.get("updateTime"));
|
|
|
int page = StringUtils.isBlank(paramMap.get("page")) ? 1 : Integer.parseInt(paramMap.get("page"));
|
|
|
if (page < 1) {
|
|
|
throw new IllegalArgumentException("分页参数不合法");
|
|
|
}
|
|
|
|
|
|
// 1) 构建ES请求
|
|
|
SearchParam searchParam = new SearchParam();
|
|
|
QueryBuilder queryBuilder = StringUtils.isNotEmpty(queryWord) ? QueryBuilders.matchQuery("source", queryWord) : QueryBuilders.matchAllQuery();
|
|
|
searchParam.setQuery(queryBuilder);
|
|
|
searchParam.setPage(page);
|
|
|
searchParam.setOffset((page - 1) * 10);
|
|
|
searchParam.setSize(10);
|
|
|
if (updateTime > 0) {
|
|
|
searchParam.setFiter(QueryBuilders.rangeQuery("updateTime").gte(updateTime));
|
|
|
}
|
|
|
|
|
|
// 2) 调用ES查询
|
|
|
SearchResult searchResult = searchCommonService.doSearch(ISearchConstants.INDEX_NAME_CONVERSION, searchParam);
|
|
|
if (searchResult == null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
// 3) 返回结果
|
|
|
Map<String, Object> dataMap = new HashMap<>();
|
|
|
dataMap.put("total", searchResult.getTotal());
|
|
|
dataMap.put("page", searchResult.getPage());
|
|
|
dataMap.put("page_size", searchParam.getSize());
|
|
|
dataMap.put("page_total", searchResult.getTotalPage());
|
|
|
dataMap.put("conversion_list", searchResult.getResultList());
|
|
|
logger.info("[func=suggestConversion][cost={}]", System.currentTimeMillis() - begin);
|
|
|
return new SearchApiResult().setData(dataMap);
|
|
|
} catch (Exception e) {
|
|
|
logger.error(e.getMessage(), e);
|
|
|
return new SearchApiResult().setCode(500).setMessage(e.getMessage()).setData(null);
|
|
|
}
|
|
|
}
|
|
|
private static final Logger logger = LoggerFactory.getLogger(SuggestServiceImpl.class);
|
|
|
|
|
|
private static final Logger CACHE_MATCH_REQUEST = LoggerFactory.getLogger("CACHE_MATCH_REQUEST");
|
|
|
|
|
|
private static final String SUGGEST_PARAM_APPTYPE = "app_type";
|
|
|
|
|
|
private static final String SUGGEST_PARAM_GLOBAL = "contain_global";
|
|
|
|
|
|
private static final int VALID_STATUS = 1;
|
|
|
|
|
|
@Autowired
|
|
|
private SearchCommonService searchCommonService;
|
|
|
|
|
|
@Autowired
|
|
|
private SearchCacheService searchCacheService;
|
|
|
|
|
|
private ApplicationEventPublisher publisher;
|
|
|
|
|
|
@Override
|
|
|
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
|
|
this.publisher = applicationEventPublisher;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public SuggestApiResult suggest(Map<String, String> paramMap) {
|
|
|
// 已使用SuggestSearchHandler进行重构
|
|
|
try {
|
|
|
logger.info("[func=suggest][param={}][begin={}]", paramMap.toString(), System.currentTimeMillis());
|
|
|
|
|
|
// 1)关键参数验证
|
|
|
String keyword = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
|
|
|
if (StringUtils.isBlank(keyword)) {
|
|
|
return new SuggestApiResult().setCode(400).setMessage("关键字[query]参数为空!");
|
|
|
}
|
|
|
keyword = keyword.toLowerCase();
|
|
|
if (keyword.length() > 30) {
|
|
|
return new SuggestApiResult().setCode(400).setMessage("关键字[query]参数非法!");
|
|
|
}
|
|
|
|
|
|
// 2)构建查询参数
|
|
|
SearchParam searchParam = buildSuggestSearchParam(paramMap);
|
|
|
|
|
|
// 3)先从缓存中获取
|
|
|
final String indexName = ISearchConstants.INDEX_NAME_SUGGEST;
|
|
|
JSONObject suggest = searchCacheService.getJSONObjectFromCache(indexName, searchParam);
|
|
|
if (suggest != null) {
|
|
|
CACHE_MATCH_REQUEST.info("match cache , url is :/suggest.json?" + HttpServletRequestUtils.genParamString(paramMap));
|
|
|
return new SuggestApiResult().setCode(200).setMessage("suggest List.").setSuggest(suggest);
|
|
|
}
|
|
|
|
|
|
// 4)进行ES检索
|
|
|
SearchResult searchResult = searchCommonService.doSearch(indexName, searchParam);
|
|
|
// 将searchResult转化为map返回--需要把aggregation转化为需要的结构
|
|
|
if (searchResult == null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
if (CollectionUtils.isEmpty(searchResult.getResultList())) {
|
|
|
// 5) suggest 纠错
|
|
|
SearchResult newSearchResult = getSpellCorrectResult(paramMap);
|
|
|
if (newSearchResult != null) {
|
|
|
searchResult = newSearchResult;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
suggest = new JSONObject();
|
|
|
List<Map<String, Object>> itemList = new ArrayList<Map<String, Object>>();
|
|
|
String countEsField = getCountField(paramMap);
|
|
|
for (Map<String, Object> map : searchResult.getResultList()) {
|
|
|
Map<String, Object> item = new HashMap<String, Object>();
|
|
|
item.put("item", map.get("keyword"));
|
|
|
item.put("frequency", map.get(countEsField));
|
|
|
item.put("type", map.get("type"));
|
|
|
itemList.add(item);
|
|
|
}
|
|
|
suggest.put("items", itemList);
|
|
|
searchCacheService.addJSONObjectToCache(indexName, searchParam, suggest);
|
|
|
return new SuggestApiResult().setCode(200).setMessage("suggest List.").setSuggest(suggest);
|
|
|
} catch (Exception e) {
|
|
|
logger.error(e.getMessage(), e);
|
|
|
publisher.publishEvent(new SearchEvent(EventReportEnum.SEARCHCONTROLLER_SUGGEST.getEventName(), EventReportEnum.SEARCHCONTROLLER_SUGGEST.getFunctionName(),
|
|
|
EventReportEnum.SEARCHCONTROLLER_SUGGEST.getMoudleName(), "exception", IgnoreSomeException.filterSomeException(e), null));
|
|
|
return new SuggestApiResult().setCode(500).setMessage(e.getMessage()).setSuggest(null);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
private SearchParam buildSuggestSearchParam(Map<String, String> paramMap) {
|
|
|
// 2)构建查询参数
|
|
|
String keyword = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD).toLowerCase();
|
|
|
SearchParam searchParam = new SearchParam();
|
|
|
int size = SearchRequestParams.RESULT_SIZE_SUGGEST;
|
|
|
if (paramMap.containsKey(SearchRequestParams.PARAM_SEARCH_SIZE)) {
|
|
|
size = Integer.parseInt(paramMap.get(SearchRequestParams.PARAM_SEARCH_SIZE));
|
|
|
}
|
|
|
if (size > 10) {
|
|
|
size = 10;
|
|
|
}
|
|
|
|
|
|
QueryBuilder query = QueryBuilders.boolQuery().should(QueryBuilders.prefixQuery("keyword", keyword)).should(QueryBuilders.prefixQuery("keyword.keyword_pinyin", keyword))
|
|
|
.should(QueryBuilders.prefixQuery("keyword.keyword_jianpin", keyword));
|
|
|
searchParam.setQuery(query);
|
|
|
searchParam.setPage(1);
|
|
|
searchParam.setSize(size);
|
|
|
|
|
|
// count数量要>=2
|
|
|
final String countEsField = getCountField(paramMap);
|
|
|
BoolQueryBuilder boolFilter = QueryBuilders.boolQuery();
|
|
|
boolFilter.must(QueryBuilders.termQuery("status", VALID_STATUS));
|
|
|
boolFilter.must(QueryBuilders.rangeQuery(countEsField).gte(2));
|
|
|
searchParam.setFiter(boolFilter);
|
|
|
|
|
|
// 3)设置排序字段
|
|
|
List<SortBuilder> sortBuilders = new ArrayList<SortBuilder>();
|
|
|
sortBuilders.add(SortBuilders.fieldSort("weight").order(SortOrder.DESC));
|
|
|
sortBuilders.add(SortBuilders.fieldSort(countEsField).order(SortOrder.DESC));
|
|
|
searchParam.setSortBuilders(sortBuilders);
|
|
|
return searchParam;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 根据搜索条件判断使用到的suggest索引的count字段名。
|
|
|
*
|
|
|
* @param paramMap 搜索条件
|
|
|
* @return count字段名
|
|
|
*/
|
|
|
@Override
|
|
|
public String getCountField(Map<String, String> paramMap) {
|
|
|
// suggest 支持PC、APP、BLK走不同的count字段 字段同SuggestIndexBO里保持一直
|
|
|
if (paramMap.containsKey(SUGGEST_PARAM_APPTYPE) && "1".equals(paramMap.get(SUGGEST_PARAM_APPTYPE))) {
|
|
|
return "countForBlk";
|
|
|
} else if (paramMap.containsKey(SUGGEST_PARAM_GLOBAL) && "Y".equals(paramMap.get(SUGGEST_PARAM_GLOBAL))) {
|
|
|
return "countForApp";
|
|
|
} else {
|
|
|
return "count";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private SearchResult getSpellCorrectResult(Map<String, String> paramMap) {
|
|
|
String keyword = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD).toLowerCase();
|
|
|
String newKeyword = getSpellingCorrectKeyword(keyword);
|
|
|
if (StringUtils.isEmpty(newKeyword)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
logger.info("Switch the suggest keyword from [{}] to [{}].", keyword, newKeyword);
|
|
|
Map<String, String> newParamMap = new HashMap<>(paramMap.size());
|
|
|
newParamMap.putAll(paramMap);
|
|
|
newParamMap.put(SearchRequestParams.PARAM_SEARCH_KEYWORD, newKeyword);
|
|
|
|
|
|
SearchParam newSearchParam = buildSuggestSearchParam(newParamMap);
|
|
|
return searchCommonService.doSearch(ISearchConstants.INDEX_NAME_SUGGEST, newSearchParam);
|
|
|
} catch (Exception e) {
|
|
|
logger.error("Get new suggest result by keyword [" + newKeyword + "] failed!", e);
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 到suggest索引进行拼写纠错处理。
|
|
|
*
|
|
|
* @param keyword 用户输入的关键词
|
|
|
* @return 纠错后的关键词。
|
|
|
*/
|
|
|
@Override
|
|
|
public String getSpellingCorrectKeyword(String keyword) {
|
|
|
try {
|
|
|
SearchParam suggestSearchParam = new SearchParam();
|
|
|
suggestSearchParam.setSize(0);
|
|
|
suggestSearchParam.setSuggestionBuilder(SuggestBuilders.termSuggestion("keyword_suggestion")
|
|
|
.text(keyword.trim().toLowerCase()).field("keyword.keyword_lowercase").size(1));
|
|
|
|
|
|
JSONObject jsonObject = searchCacheService.getJSONObjectFromCache(ISearchConstants.INDEX_NAME_SUGGEST, suggestSearchParam);
|
|
|
if (jsonObject != null) {
|
|
|
CACHE_MATCH_REQUEST.info("match cache for suggest spelling correct, keyword={}", keyword);
|
|
|
return jsonObject.getString("spellingCorrentKeyword");
|
|
|
}
|
|
|
|
|
|
SearchResult searchResult = searchCommonService.doSearch(ISearchConstants.INDEX_NAME_SUGGEST, suggestSearchParam);
|
|
|
if (searchResult == null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
String spellingCorrentKeyword = "";
|
|
|
if (searchResult.getSuggestionMap() != null && searchResult.getSuggestionMap().get("keyword_suggestion") != null) {
|
|
|
TermSuggestion suggestion = (TermSuggestion) searchResult.getSuggestionMap().get("keyword_suggestion");
|
|
|
List<TermSuggestion.Entry> entries = suggestion.getEntries();
|
|
|
if (CollectionUtils.isNotEmpty(entries)) {
|
|
|
List<TermSuggestion.Entry.Option> entryList = entries.get(0).getOptions();
|
|
|
if (CollectionUtils.isNotEmpty(entryList)) {
|
|
|
TermSuggestion.Entry.Option option = entryList.get(0);
|
|
|
spellingCorrentKeyword = option.getText().string();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
jsonObject = new JSONObject();
|
|
|
jsonObject.put("spellingCorrentKeyword", spellingCorrentKeyword);
|
|
|
searchCacheService.addJSONObjectToCache(ISearchConstants.INDEX_NAME_SUGGEST, suggestSearchParam, jsonObject);
|
|
|
return spellingCorrentKeyword;
|
|
|
} catch (Exception e) {
|
|
|
logger.error("Get spelling correct keyword by [" + keyword + "] failed!", e);
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 根据关键词查询suggest索引
|
|
|
*
|
|
|
* @param paramMap 查询参数
|
|
|
* @return 满足查询要求的建议词
|
|
|
*/
|
|
|
@Override
|
|
|
public SearchApiResult suggestList(Map<String, String> paramMap) {
|
|
|
try {
|
|
|
long begin = System.currentTimeMillis();
|
|
|
logger.info("[func=suggestList][param={}][begin={}]", paramMap, begin);
|
|
|
|
|
|
String queryWord = paramMap.get("query");
|
|
|
int page = StringUtils.isBlank(paramMap.get("page")) ? 1 : Integer.parseInt(paramMap.get("page"));
|
|
|
if (page < 1) {
|
|
|
throw new IllegalArgumentException("分页参数不合法");
|
|
|
}
|
|
|
|
|
|
// 1) 构建ES请求
|
|
|
SearchParam searchParam = new SearchParam();
|
|
|
QueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
|
|
|
if (StringUtils.isNotEmpty(queryWord)) {
|
|
|
// 是否精确匹配
|
|
|
String accurateQuery = paramMap.get("accurate");
|
|
|
queryBuilder = "Y".equalsIgnoreCase(accurateQuery) ? QueryBuilders.matchQuery("standardKeyword", CharUtils.standardized(queryWord)) : QueryBuilders.matchQuery("keyword.keyword_ik", queryWord);
|
|
|
}
|
|
|
|
|
|
searchParam.setQuery(queryBuilder);
|
|
|
searchParam.setPage(page);
|
|
|
searchParam.setOffset((page - 1) * 10);
|
|
|
searchParam.setSize(10);
|
|
|
|
|
|
// 2) 调用ES查询
|
|
|
SearchResult searchResult = searchCommonService.doSearch(ISearchConstants.INDEX_NAME_SUGGEST, searchParam);
|
|
|
if (searchResult == null) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
// 3) 返回结果
|
|
|
Map<String, Object> dataMap = new HashMap<>();
|
|
|
dataMap.put("total", searchResult.getTotal());
|
|
|
dataMap.put("page", searchResult.getPage());
|
|
|
dataMap.put("page_size", searchParam.getSize());
|
|
|
dataMap.put("page_total", searchResult.getTotalPage());
|
|
|
dataMap.put("suggest_list", searchResult.getResultList());
|
|
|
logger.info("[func=suggestList][cost={}]", System.currentTimeMillis() - begin);
|
|
|
return new SearchApiResult().setData(dataMap);
|
|
|
} catch (Exception e) {
|
|
|
logger.error(e.getMessage(), e);
|
|
|
return new SearchApiResult().setCode(500).setMessage(e.getMessage()).setData(null);
|
|
|
}
|
|
|
}
|
|
|
} |
...
|
...
|
|