Authored by 胡古飞

Merge branch 'master' into brands_new

Showing 20 changed files with 1205 additions and 567 deletions
package com.yoho.search.service.aggregations.impls;
import com.yoho.search.core.es.agg.AbstractAggregation;
import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* 单个字段进行聚合的抽象类
*/
public abstract class AbstractSingleFieldAggregation extends AbstractAggregation {
private int count;
public AbstractSingleFieldAggregation(int count) {
this.count = count;
}
@Override
public AbstractAggregationBuilder getBuilder() {
return AggregationBuilders.terms(aggName()).field(getField()).size(count);
}
@Override
public Object getAggregationResponseMap(Map<String, Aggregation> aggMaps) {
List<String> valueList = new ArrayList<String>();
MultiBucketsAggregation aggregation = this.getAggregation(aggMaps);
if (aggregation == null) {
return valueList;
}
Iterator<? extends Bucket> itSizeAgg = aggregation.getBuckets().iterator();
while (itSizeAgg.hasNext()) {
Bucket ltSize = itSizeAgg.next();
valueList.add(ltSize.getKeyAsString());
}
return valueList;
}
abstract protected String getField();
}
... ...
package com.yoho.search.service.aggregations.impls;
public class BrandNameAggregation extends AbstractSingleFieldAggregation {
public BrandNameAggregation(int count) {
super(count);
}
@Override
public String aggName() {
return "brandNameAgg";
}
@Override
protected String getField() {
return "brandName.brandName_keyword";
}
}
... ...
package com.yoho.search.service.aggregations.impls;
public class SmallSortNameAggregation extends AbstractSingleFieldAggregation {
public SmallSortNameAggregation(int count) {
super(count);
}
@Override
public String aggName() {
return "smallSortNameAgg";
}
@Override
protected String getField() {
return "smallSort.smallSort_keyword";
}
}
... ...
package com.yoho.search.service.aggregations.impls;
public class StyleNameAggregation extends AbstractSingleFieldAggregation {
public StyleNameAggregation(int count) {
super(count);
}
@Override
public String aggName() {
return "styleNameAgg";
}
@Override
protected String getField() {
return "style";
}
}
... ...
package com.yoho.search.service.restapi;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.yoho.search.service.downgrade.aop.DownGradeAble;
import com.yoho.search.service.servicenew.ISearchRecommendService;
import com.yoho.search.service.servicenew.ISuggestService;
import com.yoho.search.service.utils.HttpServletRequestUtils;
import com.yoho.search.service.vo.SearchApiResult;
import com.yoho.search.service.vo.SuggestApiResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.yoho.search.service.downgrade.aop.DownGradeAble;
import com.yoho.search.service.servicenew.ISuggestService;
import com.yoho.search.service.utils.HttpServletRequestUtils;
import com.yoho.search.service.vo.SuggestApiResult;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Controller
public class SuggestController {
... ... @@ -21,6 +21,9 @@ public class SuggestController {
@Autowired
private ISuggestService suggestService;
@Autowired
private ISearchRecommendService searchRecommendService;
/**
* 搜索建议接口
*
... ... @@ -34,4 +37,27 @@ public class SuggestController {
return suggestService.suggest(paramMap);
}
/**
* 搜索提示接口 提供给内部使用
*
* @return
*/
@RequestMapping(method = RequestMethod.GET, value = "tools/suggestConversion")
@ResponseBody
public SearchApiResult suggestConversion(HttpServletRequest request) {
Map<String, String> paramMap = HttpServletRequestUtils.transParamType(request);
return searchRecommendService.suggestConversionList(paramMap);
}
/**
* 搜索建议词接口 提供给内部使用
*
* @return
*/
@RequestMapping(method = RequestMethod.GET, value = "tools/suggestList")
@ResponseBody
public SearchApiResult suggestList(HttpServletRequest request) {
Map<String, String> paramMap = HttpServletRequestUtils.transParamType(request);
return suggestService.suggestList(paramMap);
}
}
... ...
... ... @@ -2,6 +2,7 @@ package com.yoho.search.service.restapi;
import com.yoho.search.service.cache.LocalCacheService;
import com.yoho.search.service.personalized.PersonalVectorFeatureSearch;
import com.yoho.search.service.service.SearchDynamicConfigService;
import com.yoho.search.service.service.SearchKeyWordService;
import com.yoho.search.service.servicenew.IProductListService;
import com.yoho.search.service.vo.KeyWordWithCount;
... ... @@ -9,12 +10,14 @@ import com.yoho.search.service.vo.SearchApiResult;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeResponse.AnalyzeToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
... ... @@ -31,6 +34,9 @@ public class ToolsController {
@Autowired
private IProductListService productListService;
@Autowired
private SearchDynamicConfigService searchDynamicConfigService;
/**
* 获取热搜词结果
*
... ... @@ -104,6 +110,7 @@ public class ToolsController {
/**
* 删除redis上的key
*
* @param redisKey
* @return
*/
... ... @@ -141,6 +148,23 @@ public class ToolsController {
return new SearchApiResult().setMessage("清除本地缓存成功");
}
@RequestMapping(value = "/dynamicParameterValue")
@ResponseBody
public Map<String, Object> dynamicParameterValue(String key) {
Map<String, Object> rtnMap = new HashMap<String, Object>();
try {
Assert.notNull(key);
rtnMap.put("code", 200);
rtnMap.put("value", searchDynamicConfigService.getDynamicParameterValue(key));
} catch (Exception e) {
rtnMap.put("code", 400);
rtnMap.put("msg", e.getMessage());
}
return rtnMap;
}
@RequestMapping(method = RequestMethod.GET, value = "/calVectorFeature")
@ResponseBody
public SearchApiResult calVectorFeature(@RequestParam String uid, @RequestParam String skns, @RequestParam String version) throws Exception {
... ...
package com.yoho.search.service.service;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.yoho.core.config.ConfigReader;
/**
* Created by ginozhang on 2016/12/20.
*/
@Component
public class DynamicSearchRuleHelper {
@Autowired
private ConfigReader configReader;
private static final Logger logger = LoggerFactory.getLogger(DynamicSearchRuleHelper.class);
public List<String> getDynamicSearchFields(Map<String, String> paramMap) {
// boolean supportDynamicRule =
// configReader.getBoolean("search.dynamic.rule.open", true);
// if (!supportDynamicRule) {
// return null;
// }
// String allowTestRule = paramMap.get("allowTestRule");
// RuleStatusEnum statusEnum = "Y".equals(allowTestRule) ?
// RuleStatusEnum.Tested : RuleStatusEnum.Released;
// PageTypeEnum pageTypeEnum =
// PageTypeEnum.fromPageId(paramMap.get("pageId"));
// String ruleKey = DynamicRuleKeyUtils.getKey(pageTypeEnum,
// UsageTypeEnum.SearchField, statusEnum);
// String ruleValue = configReader.getString(ruleKey, "-1");
// if (StringUtils.isEmpty(ruleValue) || "-1".equals(ruleValue)) {
// return null;
// }
// return Arrays.asList(ruleValue.split(","));
return null;
}
public String getDynamicRuleValue(Map<String, String> paramMap) {
// boolean supportDynamicRule =
// configReader.getBoolean("search.dynamic.rule.open", true);
// if (!supportDynamicRule) {
// return null;
// }
//
// String allowTestRule = paramMap.get("allowTestRule");
// RuleStatusEnum statusEnum = "Y".equals(allowTestRule) ?
// RuleStatusEnum.Tested : RuleStatusEnum.Released;
// PageTypeEnum pageTypeEnum =
// PageTypeEnum.fromPageId(paramMap.get("pageId"));
// String ruleKey = DynamicRuleKeyUtils.getKey(pageTypeEnum,
// UsageTypeEnum.ShouldField, statusEnum);
// return configReader.getString(ruleKey, "-1");
return null;
}
public QueryBuilder buildDynamicSerach(QueryBuilder queryBuilder, Map<String, String> paramMap, String ruleValue) {
// simpleRuleValue = isGlobal|Y|-300;brandId|2614,817,763,217|-20000;
// compoundRuleValue =
// _COMPOUNDRULE|brandId@12,14&smallSortId@15,16&|-300;
try {
logger.debug("Begin to process dynamic rule [{}].", ruleValue);
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(queryBuilder);
for (String ruleItem : StringUtils.split(ruleValue, ';')) {
if (StringUtils.isEmpty(ruleItem)) {
continue;
}
String[] ruleContents = StringUtils.split(ruleItem, '|');
if (ruleContents == null || ruleContents.length < 3) {
continue;
}
String field = ruleContents[0];
Float boost = Float.valueOf(ruleContents[2]);
if ("_COMPOUNDRULE".equals(field)) {
// 复合规则
processCompoundRule(boolQueryBuilder, ruleContents[1], boost);
} else {
// 简单规则
String[] values = StringUtils.split(ruleContents[1], ',');
boolQueryBuilder.should(QueryBuilders.termsQuery(field, values).boost(boost));
}
}
return boolQueryBuilder;
} catch (Exception e) {
logger.error("Process dynamic rule [" + ruleValue + "] failed.", e);
return queryBuilder;
}
}
private void processCompoundRule(BoolQueryBuilder boolQueryBuilder, String ruleContent, Float boost) {
BoolQueryBuilder nestedBoolQueryBuilder = QueryBuilders.boolQuery();
for (String compoundRuleItem : StringUtils.split(ruleContent, '&')) {
if (StringUtils.isEmpty(compoundRuleItem)) {
continue;
}
String[] compoundRuleContents = StringUtils.split(compoundRuleItem, '@');
if (compoundRuleContents == null || compoundRuleContents.length < 2) {
continue;
}
String[] values = StringUtils.split(compoundRuleContents[1], ',');
nestedBoolQueryBuilder.must(QueryBuilders.termsQuery(compoundRuleContents[0], values));
}
if (nestedBoolQueryBuilder.hasClauses()) {
boolQueryBuilder.should(nestedBoolQueryBuilder).boost(boost);
}
}
}
package com.yoho.search.service.service;
import com.yoho.core.config.ConfigReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.yoho.core.config.ConfigReader;
@Service
public class SearchDynamicConfigService {
... ... @@ -13,7 +12,7 @@ public class SearchDynamicConfigService {
/**
* 是否直接走降级开关,默认关闭
*
*
* @return
*/
public boolean directDowngrade() {
... ... @@ -22,7 +21,7 @@ public class SearchDynamicConfigService {
/**
* 是否开启个性化
*
*
* @return
*/
public boolean openPersonalized() {
... ... @@ -37,10 +36,10 @@ public class SearchDynamicConfigService {
public boolean openVectorFeaturePersonalized() {
return configReader.getBoolean("search.personalized.vectorfeature.open", true);
}
/**
* 是否开启个性化
*
*
* @return
*/
public int userPersonalizedMaxCount() {
... ... @@ -49,7 +48,7 @@ public class SearchDynamicConfigService {
/**
* 品牌降分或过滤是否打开
*
*
* @return
*/
public boolean deScoreBrandOpen() {
... ... @@ -58,16 +57,16 @@ public class SearchDynamicConfigService {
/**
* 搜索结果是否包含全球购
*
*
* @return
*/
public boolean containglobal() {
return configReader.getBoolean("search.degrade.open.containglobal", true);
}
/**
* 是否返回变价计划
*
*
* @return
*/
public boolean pricePlanOpen() {
... ... @@ -81,10 +80,10 @@ public class SearchDynamicConfigService {
public String personalizedSearchVersion(){
return configReader.getString("search.personalized.feature.version", "-1");
}
/**
* 频道降分是否打开
*
*
* @return
*/
public boolean isDeScorePhysicalChannelOpen() {
... ... @@ -93,7 +92,7 @@ public class SearchDynamicConfigService {
/**
* 频道降分权重
*
*
* @return
*/
public double getDeScorePhysicalChannelWeight() {
... ... @@ -101,11 +100,25 @@ public class SearchDynamicConfigService {
}
/**
* 是否打开搜索无结果或结果太少时的搜索词建议功能
* 是否打开搜索无结果或结果太少时的搜索提示功能
* @return
*/
public boolean isBetterSearchSuggestionOpen(){
return configReader.getBoolean("search.better.suggestion.open", true);
public boolean isSearchSuggestionTipsOpen(){
return configReader.getBoolean("search.suggestion.tips.open", true);
}
/**
* 是否支持从conversion索引获取搜索提示词
* @return
*/
public boolean isSearchSuggestionFromConversionOpen(){
return configReader.getBoolean("search.suggestion.tips.conversion.open", true);
}
/**
* 关于API查看动态参数值
*/
public String getDynamicParameterValue(String key){
return configReader.getString(key, "DEFAULT");
}
}
... ...
package com.yoho.search.service.service;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.yoho.core.redis.YHRedisTemplate;
import com.yoho.core.redis.YHZSetOperations;
import com.yoho.search.base.utils.DateStyle;
import com.yoho.search.base.utils.DateUtil;
import com.yoho.search.base.utils.ISearchConstants;
import com.yoho.search.base.utils.RedisKeys;
import com.yoho.search.core.es.IElasticsearchClient;
import com.yoho.search.core.es.impl.YohoIndexHelper;
import com.yoho.search.base.utils.RedisKeys;
import com.yoho.search.service.vo.KeyWordWithCount;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeResponse.AnalyzeToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
... ... @@ -16,10 +19,12 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
... ... @@ -72,8 +77,35 @@ public class SearchKeyWordService {
* @param keyWord
* @return
*/
public List<String> getAnalyzeTerms(String keyWord, String analyzer) {
public List<String> getAnalyzeTerms(final String keyWord, final String analyzer, boolean useCache) {
if (!useCache) {
return getAnalyzeTermsDirect(keyWord, analyzer);
}
try {
return CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, List<String>>() {
@Override
public List<String> load(String cacheKey) throws Exception {
String[] arrays = cacheKey.split("@", 2);
Assert.isTrue(arrays != null && arrays.length == 2);
return getAnalyzeTermsDirect(arrays[1], arrays[0]);
}
}).get(analyzer + "@" + keyWord);
} catch (ExecutionException e) {
logger.error(keyWord, e);
return new ArrayList<>();
}
}
public List<String> getAnalyzeTermsDirect(String keyWord, String analyzer) {
try {
if (StringUtils.isEmpty(keyWord)) {
return new ArrayList<>();
}
List<AnalyzeToken> tokens = getAnalyzeTokens(keyWord, analyzer);
List<String> results = new ArrayList<String>();
for (AnalyzeToken analyzeToken : tokens) {
... ... @@ -86,6 +118,10 @@ public class SearchKeyWordService {
}
}
public void recordSuggestTip(String queryWord) {
recordKeyWord(RedisKeys.YOHO_SEARCH_KEYWORDS_TIPS, queryWord);
}
// 异步的做法是防止redis报错影响搜索主流程
private void recordKeyWord(String redisKeyTemplate, String queryWord) {
service.submit(new Runnable() {
... ... @@ -147,6 +183,14 @@ public class SearchKeyWordService {
}
}
public Double getKeywordCount(String redisKeyTemplate, String queryWord){
try {
return yhNoSyncZSetOperations.score(RedisKeys.getRedisKey4Yesterday(redisKeyTemplate), queryWord);
}catch (Exception e){
return null;
}
}
// 获取【热搜】toplist
public Map<String, Object> getHotkeyWords(int limit, boolean isReturnTodayRecords) {
return this.getListByScoreDesc(RedisKeys.YOHO_SEARCH_KEYWORDS_HOT, limit, isReturnTodayRecords);
... ... @@ -212,12 +256,11 @@ public class SearchKeyWordService {
}
}
public String deleteRedisKey(String redisKey){
if(yhNoSyncRedisTemplate.hasKey(redisKey)){
public String deleteRedisKey(String redisKey) {
if (yhNoSyncRedisTemplate.hasKey(redisKey)) {
yhNoSyncRedisTemplate.delete(redisKey);
return "The key has been deleted succede!";
}
else{
} else {
return "The key doesn't exist.";
}
}
... ...
package com.yoho.search.service.service.helper;
import java.util.Map;
import com.yoho.search.base.utils.CharUtils;
import com.yoho.search.base.utils.ISearchConstants;
import com.yoho.search.base.utils.SearchPageIdDefine;
import com.yoho.search.service.service.SearchDynamicConfigService;
import com.yoho.search.service.utils.SearchRequestParams;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
... ... @@ -9,10 +12,9 @@ import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.yoho.search.base.utils.CharUtils;
import com.yoho.search.base.utils.ISearchConstants;
import com.yoho.search.service.service.SearchDynamicConfigService;
import com.yoho.search.service.utils.SearchRequestParams;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class SearchCommonHelper {
... ... @@ -187,6 +189,39 @@ public class SearchCommonHelper {
}
/**
* 从搜索的关键词中提取出skn信息
*
* @param keyword
* @return
*/
public List<Integer> getQuerySknList(String keyword) {
List<Integer> sknList = new ArrayList<>();
try {
String[] keys = null;
if (keyword.contains(" ")) {
keys = keyword.split(" ");
} else if (keyword.contains(",")) {
keys = keyword.split(",");
} else if (keyword.contains("+")) {
keys = keyword.split("\\+");
}
if(keys != null && keys.length > 0){
for(String key : keys){
if (CharUtils.isNumeric(key) && key.length() >= 7) {
sknList.add(Integer.valueOf(key));
}
}
}
return sknList;
} catch (Exception e) {
return sknList;
}
}
/**
* 是否是新品到着页
*
* @param paramMap
... ... @@ -194,7 +229,7 @@ public class SearchCommonHelper {
*/
public boolean isNewRecPage(Map<String, String> paramMap) {
String pageId = paramMap.get("pageId");
if (StringUtils.isBlank(pageId) || !pageId.equals("4")) {
if (StringUtils.isBlank(pageId) || !pageId.equals(SearchPageIdDefine.PAGE_ID_NEW)) {
return false;
}
return true;
... ...
package com.yoho.search.service.service.helper;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.yoho.search.base.utils.ConvertUtils;
import com.yoho.search.base.utils.DateUtil;
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.service.personalized.PersonalVectorFeatureSearch;
import com.yoho.search.service.service.DynamicSearchRuleHelper;
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.utils.SearchRequestParams;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
@Service
public class SearchServiceHelper {
... ... @@ -45,14 +31,10 @@ public class SearchServiceHelper {
@Autowired
private SearchKeyWordService searchKeyWordService;
@Autowired
private PersonalVectorFeatureSearch personalFeatureFactorSearch;
@Autowired
private SearchDynamicConfigService dynamicConfig;
@Autowired
private SearchCommonService searchCommonService;
@Autowired
private DynamicSearchRuleHelper dynamicSearchRuleHelper;
@Autowired
private FunctionScoreSearchHelper functionScoreSearchHelper;
/**
... ... @@ -187,10 +169,7 @@ public class SearchServiceHelper {
* @param queryBuilder
*/
public void setDefaultSearchField(MultiMatchQueryBuilder queryBuilder, Map<String, String> paramMap) {
List<String> fields = dynamicSearchRuleHelper.getDynamicSearchFields(paramMap);
if (CollectionUtils.isEmpty(fields)) {
fields = ISearchConstants.SEARCH_DEFAULT_FIELD;
}
List<String> fields = ISearchConstants.SEARCH_DEFAULT_FIELD;
for (String field : fields) {
String[] fieldBoost = field.split("\\^");
if (fieldBoost.length == 2) {
... ... @@ -221,10 +200,13 @@ public class SearchServiceHelper {
}
// 全局降分品牌过滤
if ("Y".equals(paramMap.get(SearchRequestParams.PARAM_SEARCH_GLOBAL_FILTER_BRAND))) {
boolFilter.mustNot(QueryBuilders.termQuery("isForbiddenSortBrand", 1));
// if ("Y".equals(paramMap.get(SearchRequestParams.PARAM_SEARCH_GLOBAL_FILTER_BRAND))) {
// boolFilter.mustNot(QueryBuilders.termQuery("isForbiddenSortBrand", 1));
// }
if ("Y".equals(paramMap.get(SearchRequestParams.PARAM_SEARCH_GLOBAL_FILTER_BRAND)) && StringUtils.isNotBlank(paramMap.get("pageId"))){
boolFilter.mustNot(QueryBuilders.termQuery("forbiddenPageIds",paramMap.get("pageId")));
}
// 店铺
if (paramMap.containsKey(SearchRequestParams.PARAM_SEARCH_SHOP) && StringUtils.isNotBlank(paramMap.get(SearchRequestParams.PARAM_SEARCH_SHOP))
&& !SearchRequestParams.PARAM_SEARCH_SHOP.equals(filterParamName)) {
... ... @@ -757,6 +739,10 @@ public class SearchServiceHelper {
productMap.put(extendedField, map.get(extendedField));
}
}
// 增加小分类名称用于搜索推荐
productMap.put("small_sort_name", map.get("smallSort"));
return productMap;
}
}
... ...
package com.yoho.search.service.servicenew;
import com.alibaba.fastjson.JSONObject;
import com.yoho.search.service.vo.SearchApiResult;
import java.util.Map;
/**
* Created by ginozhang on 2017/3/22.
*/
public interface ISearchRecommendService {
/**
* 根据query词获取term建议和phrase建议。
* 用于搜索结果数量太少或者无结果的时候给予用户的搜索建议。
* @param searchResult 搜索结果
* @param paramMap 搜索参数
* @return 包括term建议和phrase建议。
*/
JSONObject recommend(SearchApiResult searchResult, Map<String, String> paramMap);
/**
* 根据关键词和时间查询关键词转换关系
* @param paramMap 查询参数
* @return 满足查询要求的转换关系
*/
SearchApiResult suggestConversionList(Map<String, String> paramMap);
}
... ...
package com.yoho.search.service.servicenew;
import com.alibaba.fastjson.JSONObject;
import com.yoho.search.service.vo.SearchApiResult;
import com.yoho.search.service.vo.SuggestApiResult;
import java.util.Map;
... ... @@ -16,10 +16,24 @@ public interface ISuggestService {
public SuggestApiResult suggest(Map<String, String> paramMap);
/**
* 根据query词获取term建议和phrase建议。
* 用于搜索结果数量太少或者无结果的时候给予用户的搜索建议。
* @param paramMap 搜索参数
* @return 包括term建议和phrase建议。
* 根据关键词查询suggest索引
*
* @param paramMap 查询参数
* @return 满足查询要求的建议词
*/
SearchApiResult suggestList(Map<String, String> paramMap);
/**
* 到suggest索引进行拼写纠错处理。
* @param keyword 用户输入的关键词
* @return 纠错后的关键词。
*/
String getSpellingCorrectKeyword(String keyword);
/**
* 根据搜索条件判断使用到的suggest索引的count字段名。
* @param paramMap 搜索条件
* @return count字段名
*/
JSONObject suggestByTerms(Map<String, String> paramMap);
String getCountField(Map<String, String> paramMap);
}
... ...
... ... @@ -66,8 +66,8 @@ public class GoodProductListService implements IGoodProductsService {
private final int maxSmallSortCount = 20;
private final int maxProductSknCountPerSort = 5;
private final int maxCountPerGroup = 10;
private final float firstSknScore = 300;
private final int maxCountPerGroup = 3;
// private final float firstSknScore = 300;
private final float recommendedSknMaxScore = 200;
@Override
... ... @@ -80,7 +80,7 @@ public class GoodProductListService implements IGoodProductsService {
}
// 2、先获取用户浏览的SKN对应的品类列表
List<Integer> smallSortIds = this.getProductSknSmallSortIds(paramMap, maxSmallSortCount);
// 3、再每个品类下获取5个SKN
// 3、再每个品类下获取5个SKN[每次随机打乱]
List<String> recommondSkns = this.getRecommondedSkns(smallSortIds, maxProductSknCountPerSort, paramMap);
// 4、构造搜索参数
... ... @@ -126,11 +126,14 @@ public class GoodProductListService implements IGoodProductsService {
private QueryBuilder builderGoodProductQueryBuilder(Map<String, String> paramMap, List<String> recommendedSknList) {
QueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(queryBuilder);
// 针对参数里第一个SKN加分
String productSkns = paramMap.get(SearchRequestParams.PARAM_SYNC_SKN);
if (!StringUtils.isBlank(productSkns)) {
functionScoreQueryBuilder.add(QueryBuilders.termsQuery("productSkn", productSkns.split(",")[0]), ScoreFunctionBuilders.weightFactorFunction(firstSknScore));
}
// // 针对参数里第一个SKN加分
// String productSkns =
// paramMap.get(SearchRequestParams.PARAM_SYNC_SKN);
// if (!StringUtils.isBlank(productSkns)) {
// functionScoreQueryBuilder.add(QueryBuilders.termsQuery("productSkn",
// productSkns.split(",")[0]),
// ScoreFunctionBuilders.weightFactorFunction(firstSknScore));
// }
// 针对推荐出来的SKN做加分
if (recommendedSknList != null && !recommendedSknList.isEmpty()) {
Map<Integer, List<String>> recommondSknMap = this.splitProductSkns(recommendedSknList, maxCountPerGroup);
... ... @@ -317,9 +320,14 @@ public class GoodProductListService implements IGoodProductsService {
return result;
}
/**
* 获取聚合出来的商品
*
* @param aggregation
* @return
*/
private List<String> getRecommendedSknList(MultiBucketsAggregation aggregation) {
Iterator<? extends Bucket> itAgg = aggregation.getBuckets().iterator();
// 获取聚合出来的商品
List<String> recommendedSknList = new ArrayList<String>();
while (itAgg.hasNext()) {
Bucket lt = itAgg.next();
... ...
... ... @@ -14,7 +14,7 @@ import com.yoho.search.service.service.helper.SearchCommonHelper;
import com.yoho.search.service.service.helper.SearchServiceHelper;
import com.yoho.search.service.service.helper.SearchSortHelper;
import com.yoho.search.service.servicenew.IProductListService;
import com.yoho.search.service.servicenew.ISuggestService;
import com.yoho.search.service.servicenew.ISearchRecommendService;
import com.yoho.search.service.utils.HttpServletRequestUtils;
import com.yoho.search.service.utils.SearchApiResultUtils;
import com.yoho.search.service.utils.SearchRequestParams;
... ... @@ -33,196 +33,199 @@ import java.util.*;
@Service
public class ProductListServiceImpl implements IProductListService {
private static final Logger logger = LoggerFactory.getLogger(ProductListServiceImpl.class);
private static Logger CACHE_MATCH_REQUEST = LoggerFactory.getLogger("CACHE_MATCH_REQUEST");
// 当少于20个商品时 返回智能搜索词提示
private static final int SMART_SUGGESTION_PRODUCT_LIMIT = 20;
@Autowired
private SearchCommonHelper searchCommonHelper;
@Autowired
private SearchServiceHelper searchServiceHelper;
@Autowired
private SearchSortHelper searchSortHelper;
@Autowired
private SearchCommonService searchCommonService;
@Autowired
private SearchKeyWordService searchKeyWordService;
@Autowired
private SearchCacheService searchCacheService;
@Autowired
private ISuggestService suggestService;
@Autowired
private SearchDynamicConfigService searchDynamicConfigService;
@Override
public SearchApiResult productList(Map<String, String> paramMap) {
try {
SearchApiResult searchResult = innerProductList(paramMap);
if (needTermSuggestion(searchResult, paramMap)) {
// 当商品数量太少或无结果时 支持智能推荐Term搜索
JSONObject dataMap = ((JSONObject) searchResult.getData());
dataMap.put("suggestion", suggestService.suggestByTerms(paramMap));
}
return searchResult;
} catch (Exception e) {
logger.error("[func=productList][params=" + paramMap + "]", e);
return SearchApiResultUtils.errorSearchApiResult("productList", paramMap, e);
}
}
private SearchApiResult innerProductList(Map<String, String> paramMap) throws Exception {
logger.info("[func=productList][param={}][begin={}]", paramMap, System.currentTimeMillis());
// 1)构造搜索参数
SearchParam searchParam = buildProductListSearchParam(paramMap);
// 5)从缓存中获取数据
final String indexName = ISearchConstants.INDEX_NAME_PRODUCT_INDEX;
CacheEnum cacheEnum = CacheEnum.EHCACHE;
JSONObject cacheObject = searchCacheService.getJSONObjectFromCache(cacheEnum, indexName, searchParam);
if (cacheObject != null) {
CACHE_MATCH_REQUEST.info("match cache , url is :/productindex/productList.json?" + HttpServletRequestUtils.genParamString(paramMap));
return new SearchApiResult().setData(cacheObject);
}
// 6)查询ES
SearchResult searchResult = searchCommonService.doSearch(indexName, searchParam);
if (searchResult == null) {
return new SearchApiResult().setCode(500).setMessage("execption");
}
// 7)记录关键字对应的查询结果
String queryWord = paramMap.get("query");
if (!StringUtils.isBlank(queryWord) && !searchCommonHelper.isQuerySkn(queryWord)) {
searchKeyWordService.recordKeyWordByResultCount(queryWord, searchResult.getTotal());
}
// 8)当返回结果为空时,先记录请求参数,然后修改operator为or并重新搜索,minimum修改为50%
// if (StringUtils.isNotBlank(queryWord) && 0 == searchResult.getTotal()) {
// searchKeyWordService.handleEmptyRecords(paramMap);
// logger.info("search result is empty by operator of AND, will use operator of OR to reenforce search result");
// searchParam.setQuery(searchServiceHelper.constructOrQueryBuilderForProductList(paramMap));
// searchResult = searchCommonService.doSearch(indexName, searchParam);
// }
// 9)构造返回结果
JSONObject dataMap = new JSONObject();
dataMap.put("total", searchResult.getTotal());
dataMap.put("page", searchResult.getPage());
dataMap.put("page_size", searchParam.getSize());
dataMap.put("page_total", searchResult.getTotalPage());
dataMap.put("product_list", searchServiceHelper.getProductMapList(searchResult.getResultList()));
// 10)将结果存进缓存
searchCacheService.addJSONObjectToCache(cacheEnum, indexName, searchParam, dataMap);
return new SearchApiResult().setData(dataMap);
}
private boolean needTermSuggestion(SearchApiResult searchResult, Map<String, String> paramMap) {
if (searchResult == null || searchResult.getCode() != 200 || searchResult.getData() == null) {
return false;
}
// 1. 判断是否需要进行term推荐
// 1.1 query不为空且不是查询SKN
String queryWord = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
if (StringUtils.isEmpty(queryWord) || searchCommonHelper.isQuerySkn(queryWord)) {
return false;
}
// 1.2 请求制定需要返回term推荐
if (!"Y".equalsIgnoreCase(paramMap.get(SearchRequestParams.PARAM_TERM_SUGGESTION))) {
return false;
}
// 1.3 搜索的数量小于20条
JSONObject dataMap = ((JSONObject) searchResult.getData());
if (dataMap.getIntValue("total") >= SMART_SUGGESTION_PRODUCT_LIMIT) {
return false;
}
//1.4 打开智能推荐全局开关
return searchDynamicConfigService.isBetterSearchSuggestionOpen();
}
@Override
public SearchApiResult productListBySknList(String skns) {
Assert.notNull(skns);
List<String> sknList = Arrays.asList(skns.split(","));
SearchParam searchParam = new SearchParam();
searchParam.setSize(sknList.size());
searchParam.setQuery(QueryBuilders.termsQuery("productSkn.productSkn_ansj", sknList));
SearchResult searchResult = searchCommonService.doSearch(ISearchConstants.INDEX_NAME_PRODUCT_INDEX, searchParam);
if (searchResult == null) {
return new SearchApiResult().setCode(500).setMessage("execption");
}
Map<String, String> productVectorFeatureMap = new HashMap<>(sknList.size());
searchResult.getResultList().forEach(data -> {
productVectorFeatureMap.put("" + data.get("productSkn"), "" + data.get("productFeatureFactor"));
});
return new SearchApiResult().setData(productVectorFeatureMap);
}
@Override
public SearchApiResult getESDsl(Map<String, String> paramMap) {
logger.info("[func=getESDsl][param={}]", paramMap);
try {
SearchParam searchParam = buildProductListSearchParam(paramMap);
SearchSourceBuilder searchSourceBuilder = SearchParamUtils.genSearchSourceBuilderFromSearchParam(searchParam);
SearchApiResult searchApiResult = new SearchApiResult();
searchApiResult.setData(searchSourceBuilder.toString());
logger.info("[func=getESDsl][dsl=\n{}]", searchApiResult.getData());
return searchApiResult;
} catch (Exception e) {
return SearchApiResultUtils.errorSearchApiResult("getESDsl", paramMap, e);
}
}
private void setHighlight(final Map<String, String> paramMap, SearchParam searchParam) {
if (StringUtils.isNotBlank(paramMap.get("highlight")) && "1".equals(paramMap.get("highlight")) && StringUtils.isNotBlank(paramMap.get("query"))) {
searchParam.setHighlight(true);
List<String> highlightFields = new ArrayList<String>();
highlightFields.add("productName.productName_ansj");
searchParam.setHighlightFields(highlightFields);
}
}
private SearchParam buildProductListSearchParam(Map<String, String> paramMap) throws Exception {
// 1)验证查询条数
int pageSize = StringUtils.isBlank(paramMap.get("viewNum")) ? 10 : Integer.parseInt(paramMap.get("viewNum"));
int page = StringUtils.isBlank(paramMap.get("page")) ? 1 : Integer.parseInt(paramMap.get("page"));
if (page < 1 || pageSize < 0) {
throw new IllegalArgumentException("分页参数不合法");
}
if (pageSize > 500) {
pageSize = 500;
}
// 2)构建基本查询参数
SearchParam searchParam = new SearchParam();
searchParam.setQuery(searchServiceHelper.constructQueryBuilderForProductList(paramMap));
searchParam.setFiter(searchServiceHelper.constructFilterBuilder(paramMap, null));
setHighlight(paramMap, searchParam);
searchParam.setAggregationBuilders(null);
searchParam.setPage(page);
searchParam.setOffset((page - 1) * pageSize);
searchParam.setSize(pageSize);
// 3)设置排序字段
searchParam.setSortBuilders(searchSortHelper.buildSortList(paramMap));
// 4)设置查询结果返回字段
if (StringUtils.isNotBlank(paramMap.get("resultFields"))) {
String resultFields = paramMap.get("resultFields");
searchParam.setResultFields(Arrays.asList(resultFields.split(",")));
}
return searchParam;
}
private static final Logger logger = LoggerFactory.getLogger(ProductListServiceImpl.class);
private static Logger CACHE_MATCH_REQUEST = LoggerFactory.getLogger("CACHE_MATCH_REQUEST");
// 当少于20个商品时 返回智能搜索词提示
private static final int SMART_SUGGESTION_PRODUCT_LIMIT = 20;
@Autowired
private SearchCommonHelper searchCommonHelper;
@Autowired
private SearchServiceHelper searchServiceHelper;
@Autowired
private SearchSortHelper searchSortHelper;
@Autowired
private SearchCommonService searchCommonService;
@Autowired
private SearchKeyWordService searchKeyWordService;
@Autowired
private SearchCacheService searchCacheService;
@Autowired
private ISearchRecommendService searchRecommendService;
@Autowired
private SearchDynamicConfigService searchDynamicConfigService;
@Override
public SearchApiResult productList(Map<String, String> paramMap) {
try {
SearchApiResult searchResult = innerProductList(paramMap);
if (needTermSuggestion(searchResult, paramMap)) {
// 当商品数量太少或无结果时 支持智能推荐Term搜索
JSONObject dataMap = ((JSONObject) searchResult.getData());
dataMap.put("suggestion", searchRecommendService.recommend(searchResult, paramMap));
}
return searchResult;
} catch (Exception e) {
logger.error("[func=productList][params=" + paramMap + "]", e);
return SearchApiResultUtils.errorSearchApiResult("productList", paramMap, e);
}
}
private SearchApiResult innerProductList(Map<String, String> paramMap) throws Exception {
long begin = System.currentTimeMillis();
logger.info("[func=productList][param={}][begin={}]", paramMap, begin);
// 1)构造搜索参数
SearchParam searchParam = buildProductListSearchParam(paramMap);
// 5)从缓存中获取数据
final String indexName = ISearchConstants.INDEX_NAME_PRODUCT_INDEX;
CacheEnum cacheEnum = CacheEnum.EHCACHE;
JSONObject cacheObject = searchCacheService.getJSONObjectFromCache(cacheEnum, indexName, searchParam);
if (cacheObject != null) {
CACHE_MATCH_REQUEST.info("match cache , url is :/productindex/productList.json?" + HttpServletRequestUtils.genParamString(paramMap));
return new SearchApiResult().setData(cacheObject);
}
// 6)查询ES
SearchResult searchResult = searchCommonService.doSearch(indexName, searchParam);
if (searchResult == null) {
return new SearchApiResult().setCode(500).setMessage("execption");
}
// 7)记录关键字对应的查询结果
String queryWord = paramMap.get("query");
if (!StringUtils.isBlank(queryWord) && !searchCommonHelper.isQuerySkn(queryWord)) {
searchKeyWordService.recordKeyWordByResultCount(queryWord, searchResult.getTotal());
}
// 8)当返回结果为空时,先记录请求参数,然后修改operator为or并重新搜索,minimum修改为50%
// if (StringUtils.isNotBlank(queryWord) && 0 ==
// searchResult.getTotal()) {
// searchKeyWordService.handleEmptyRecords(paramMap);
// logger.info("search result is empty by operator of AND, will use operator of OR to reenforce search result");
// searchParam.setQuery(searchServiceHelper.constructOrQueryBuilderForProductList(paramMap));
// searchResult = searchCommonService.doSearch(indexName, searchParam);
// }
// 9)构造返回结果
JSONObject dataMap = new JSONObject();
dataMap.put("total", searchResult.getTotal());
dataMap.put("page", searchResult.getPage());
dataMap.put("page_size", searchParam.getSize());
dataMap.put("page_total", searchResult.getTotalPage());
dataMap.put("product_list", searchServiceHelper.getProductMapList(searchResult.getResultList()));
// 10)将结果存进缓存
searchCacheService.addJSONObjectToCache(cacheEnum, indexName, searchParam, dataMap);
logger.info("[func=productList][cost={}]", System.currentTimeMillis() - begin);
return new SearchApiResult().setData(dataMap);
}
private boolean needTermSuggestion(SearchApiResult searchResult, Map<String, String> paramMap) {
if (searchResult == null || searchResult.getCode() != 200 || searchResult.getData() == null) {
return false;
}
// 1. 判断是否需要进行term推荐
// 1.1 query不为空
String queryWord = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
if (StringUtils.isEmpty(queryWord) || (queryWord.length() > 30 && !searchCommonHelper.isQuerySkn(queryWord))) {
return false;
}
// 1.2 请求制定需要返回term推荐
if (!"Y".equalsIgnoreCase(paramMap.get(SearchRequestParams.PARAM_TERM_SUGGESTION))) {
return false;
}
// 1.3 搜索的数量小于20条
JSONObject dataMap = ((JSONObject) searchResult.getData());
if (dataMap.getIntValue("total") >= SMART_SUGGESTION_PRODUCT_LIMIT) {
return false;
}
// 1.4 打开智能推荐全局开关
return searchDynamicConfigService.isSearchSuggestionTipsOpen();
}
@Override
public SearchApiResult productListBySknList(String skns) {
Assert.notNull(skns);
List<String> sknList = Arrays.asList(skns.split(","));
SearchParam searchParam = new SearchParam();
searchParam.setSize(sknList.size());
searchParam.setQuery(QueryBuilders.termsQuery("productSkn.productSkn_ansj", sknList));
SearchResult searchResult = searchCommonService.doSearch(ISearchConstants.INDEX_NAME_PRODUCT_INDEX, searchParam);
if (searchResult == null) {
return new SearchApiResult().setCode(500).setMessage("execption");
}
Map<String, String> productVectorFeatureMap = new HashMap<>(sknList.size());
searchResult.getResultList().forEach(data -> {
productVectorFeatureMap.put("" + data.get("productSkn"), "" + data.get("productFeatureFactor"));
});
return new SearchApiResult().setData(productVectorFeatureMap);
}
@Override
public SearchApiResult getESDsl(Map<String, String> paramMap) {
logger.info("[func=getESDsl][param={}]", paramMap);
try {
SearchParam searchParam = buildProductListSearchParam(paramMap);
SearchSourceBuilder searchSourceBuilder = SearchParamUtils.genSearchSourceBuilderFromSearchParam(searchParam);
SearchApiResult searchApiResult = new SearchApiResult();
searchApiResult.setData(searchSourceBuilder.toString());
logger.info("[func=getESDsl][dsl=\n{}]", searchApiResult.getData());
return searchApiResult;
} catch (Exception e) {
return SearchApiResultUtils.errorSearchApiResult("getESDsl", paramMap, e);
}
}
private void setHighlight(final Map<String, String> paramMap, SearchParam searchParam) {
if (StringUtils.isNotBlank(paramMap.get("highlight")) && "1".equals(paramMap.get("highlight")) && StringUtils.isNotBlank(paramMap.get("query"))) {
searchParam.setHighlight(true);
List<String> highlightFields = new ArrayList<String>();
highlightFields.add("productName.productName_ansj");
searchParam.setHighlightFields(highlightFields);
}
}
private SearchParam buildProductListSearchParam(Map<String, String> paramMap) throws Exception {
// 1)验证查询条数
int pageSize = StringUtils.isBlank(paramMap.get("viewNum")) ? 10 : Integer.parseInt(paramMap.get("viewNum"));
int page = StringUtils.isBlank(paramMap.get("page")) ? 1 : Integer.parseInt(paramMap.get("page"));
if (page < 1 || pageSize < 0) {
throw new IllegalArgumentException("分页参数不合法");
}
if (pageSize > 500) {
pageSize = 500;
}
// 2)构建基本查询参数
SearchParam searchParam = new SearchParam();
searchParam.setQuery(searchServiceHelper.constructQueryBuilderForProductList(paramMap));
searchParam.setFiter(searchServiceHelper.constructFilterBuilder(paramMap, null));
setHighlight(paramMap, searchParam);
searchParam.setAggregationBuilders(null);
searchParam.setPage(page);
searchParam.setOffset((page - 1) * pageSize);
searchParam.setSize(pageSize);
// 3)设置排序字段
searchParam.setSortBuilders(searchSortHelper.buildSortList(paramMap));
// 4)设置查询结果返回字段
if (StringUtils.isNotBlank(paramMap.get("resultFields"))) {
String resultFields = paramMap.get("resultFields");
searchParam.setResultFields(Arrays.asList(resultFields.split(",")));
}
return searchParam;
}
}
... ...
package com.yoho.search.service.servicenew.impl;
import com.alibaba.fastjson.JSONObject;
import com.yoho.search.base.utils.CharUtils;
import com.yoho.search.base.utils.ISearchConstants;
import com.yoho.search.base.utils.RedisKeys;
import com.yoho.search.core.es.model.SearchParam;
import com.yoho.search.core.es.model.SearchResult;
import com.yoho.search.service.service.SearchCacheService;
import com.yoho.search.service.service.SearchCommonService;
import com.yoho.search.service.service.SearchKeyWordService;
import com.yoho.search.service.service.helper.SearchCommonHelper;
import com.yoho.search.service.service.helper.SearchServiceHelper;
import com.yoho.search.service.servicenew.ISearchRecommendService;
import com.yoho.search.service.servicenew.ISuggestService;
import com.yoho.search.service.utils.SearchRequestParams;
import com.yoho.search.service.vo.SearchApiResult;
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.*;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.util.*;
import java.util.stream.Collectors;
/**
* Created by ginozhang on 2017/3/22.
*/
@Component
public class SearchRecommendServiceImpl implements ISearchRecommendService {
private static final Logger logger = LoggerFactory.getLogger(SearchRecommendServiceImpl.class);
private static final Logger CACHE_MATCH_REQUEST = LoggerFactory.getLogger("CACHE_MATCH_REQUEST");
// 返回智能搜索词的数量
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("潮流", "时尚", "休闲");
private static final Map<String, String> CUSTOM_SUGGEST_CONVERSIONS = new HashMap<>();
private static final int VALID_STATUS = 1;
@Autowired
private ISuggestService suggestService;
@Autowired
private SearchCommonService searchCommonService;
@Autowired
private SearchCommonHelper searchCommonHelper;
@Autowired
private SearchCacheService searchCacheService;
@Autowired
private SearchKeyWordService searchKeyWordService;
@Autowired
private SearchServiceHelper searchServiceHelper;
static {
// it是一个停用词 所以直接配置转换关系
CUSTOM_SUGGEST_CONVERSIONS.put("it", ":CHOCOOLATE,izzue,5cm");
}
/**
* 根据query关键词和搜索结果获取搜索推荐词。
* 用于搜索结果数量太少或者无结果的时候给予用户的搜索建议。
*
* @param searchResult 搜索结果
* @param paramMap 商品列表搜索参数
* @return 包括term建议和phrase建议。
*/
@Override
public JSONObject recommend(SearchApiResult searchResult, Map<String, String> paramMap) {
String queryWord = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
long begin = System.currentTimeMillis();
logger.info("[func=recommend][queryWord={}][begin={}]", queryWord, begin);
try {
Assert.isTrue(StringUtils.isNotEmpty(queryWord), "query keyword cannot be empty.");
Assert.isTrue(searchResult != null && searchResult.getData() != null, "SearchResult is invalid.");
String keywordsToSearch = queryWord;
// 搜索推荐分两部分
// 1) 第一部分是最常见的情况,包括有结果、根据SKN搜索、关键词未出现在空结果Redis ZSet里
if (containsProductInSearchResult(searchResult)) {
// 1.1) 搜索有结果的 优先从搜索结果聚合出品牌等关键词进行查询
JSONObject dataMap = ((JSONObject) searchResult.getData());
List<Map<String, Object>> productList = (List<Map<String, Object>>) dataMap.get("product_list");
String aggKeywords = aggKeywordsByProductList(productList);
keywordsToSearch = keywordsToSearch + " " + aggKeywords;
} else if (isQuerySkn(queryWord)) {
// 1.2) 如果是查询SKN 没有查询到的 后续的逻辑也无法推荐 所以直接到ES里去获取关键词
keywordsToSearch = aggKeywordsBySkns(queryWord);
if (StringUtils.isEmpty(keywordsToSearch)) {
return defaultSuggestRecommendation();
}
}
Double count = searchKeyWordService.getKeywordCount(RedisKeys.YOHO_SEARCH_KEYWORDS_EMPTY, queryWord);
if (count == null || queryWord.length() >= 5) {
// 1.3) 如果该关键词一次都没有出现在空结果列表或者长度大于5 则该词很有可能是可以搜索出结果的 因此优先取suggest去搜索一把 减少后面的查询动作
JSONObject recommendResult = recommendBySuggestIndex(paramMap, keywordsToSearch, false);
if (recommendResult == null) {
return defaultSuggestRecommendation();
} else if (CollectionUtils.isNotEmpty((List) recommendResult.get("terms_suggestion"))) {
logger.info("[func=recommend][queryWord={}][cost={}]", queryWord, System.currentTimeMillis() - begin);
return recommendResult;
}
}
// 2) 第二部分是通过Conversion和拼写纠错去获取关键词 由于很多品牌的拼写可能比较相近 因此先走Conversion然后再拼写检查
String spellingCorrentWord = null, dest = null;
if (allowGetingDest(queryWord) && StringUtils.isNotEmpty((dest = getSuggestConversionDestBySource(queryWord)))) {
// 2.1) 爬虫和自定义的Conversion处理
keywordsToSearch = dest;
} else if (allowSpellingCorrent(queryWord) && StringUtils.isNotEmpty((spellingCorrentWord = suggestService.getSpellingCorrectKeyword(queryWord)))) {
// 2.2) 执行拼写检查 由于在搜索建议的时候会进行拼写检查 所以缓存命中率高
keywordsToSearch = spellingCorrentWord;
} else {
// 2.3) 如果两者都没有 则直接返回
return defaultSuggestRecommendation();
}
JSONObject recommendResult = recommendBySuggestIndex(paramMap, keywordsToSearch, dest != null);
if (recommendResult == null || CollectionUtils.isEmpty((List) recommendResult.get("terms_suggestion"))) {
recommendResult = defaultSuggestRecommendation();
}
logger.info("[func=recommend][queryWord={}][cost={}]", queryWord, System.currentTimeMillis() - begin);
return recommendResult;
} catch (Exception e) {
logger.error("[func=recommend][queryWord=" + queryWord + "]", e);
return defaultSuggestRecommendation();
}
}
private boolean allowGetingDest(String queryWord) {
// 对于conversion长度要求在[2,10]之间
if (queryWord.length() < 2 || queryWord.length() > 10) {
return false;
}
// 不能有空格、加号、逗号等符号
if (queryWord.contains(" ") || queryWord.contains("+") || queryWord.contains(",") || queryWord.contains("%")) {
return false;
}
return true;
}
private boolean allowSpellingCorrent(String queryWord) {
// 对于拼写检查长度要求在[4,20]之间
if (queryWord.length() < 4 || queryWord.length() > 20) {
return false;
}
// 不能有空格、加号、逗号等符号
if (queryWord.contains(" ") || queryWord.contains("+") || queryWord.contains(",") || queryWord.contains("%")) {
return false;
}
return true;
}
/**
* 根据计算获取的推荐词列表到suggest索引获取推荐词。
* 多个词的情况下优先会给匹配到更多term的词加分。
*
* @param paramMap 搜索参数,用于判断取哪个count
* @param keywordsToSearch 计算获取的推荐词列表
* @param isLimitKeywords 是否限制只能是推荐词列表里面的词
* @return 搜推荐结果
*/
private JSONObject recommendBySuggestIndex(Map<String, String> paramMap, String keywordsToSearch, boolean isLimitKeywords) {
String srcQueryWord = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
long begin = System.currentTimeMillis();
logger.info("[func=recommendBySuggestIndex][srcQueryWord={}][keywordsToSearch={}][begin={}]", srcQueryWord, keywordsToSearch, begin);
// 1) 如果是查询SKN 直接返回 前面已经处理过了
if (StringUtils.isEmpty(keywordsToSearch)) {
return null;
}
// 2) 先对keywordsToSearch进行分词
List<String> terms = null;
if (isLimitKeywords) {
terms = Arrays.stream(keywordsToSearch.split(",")).filter(term -> term != null && term.length() > 1).distinct().collect(Collectors.toList());
} else {
terms = searchKeyWordService.getAnalyzeTerms(keywordsToSearch, "ik_smart", true);
}
if (CollectionUtils.isEmpty(terms)) {
return new JSONObject();
}
Set<String> termSet = terms.stream().collect(Collectors.toSet());
logger.info("[func=recommendBySuggestIndex][termSet={}]", termSet);
// 3) 根据terms搜索构造搜索请求
final String countField = suggestService.getCountField(paramMap);
SearchParam searchParam = new SearchParam();
QueryBuilder queryBuilder = isLimitKeywords ? buildQueryBuilderByLimit(terms) : buildQueryBuilder(keywordsToSearch, termSet);
searchParam.setQuery(queryBuilder);
searchParam.setPage(1);
searchParam.setSize(SMART_SUGGESTION_TERM_COUNT);
// 3.5) 设置过滤条件
BoolQueryBuilder boolFilter = QueryBuilders.boolQuery();
boolFilter.must(QueryBuilders.termQuery("status", VALID_STATUS));
boolFilter.must(QueryBuilders.rangeQuery(countField).gte(SMART_SUGGESTION_COUNT_LIMIT));
boolFilter.mustNot(QueryBuilders.termQuery("standardKeyword", CharUtils.standardized(srcQueryWord)));
if (isLimitKeywords) {
boolFilter.must(QueryBuilders.termsQuery("standardKeyword", termSet.stream().map(term -> CharUtils.standardized(term)).collect(Collectors.toList())));
}
searchParam.setFiter(boolFilter);
// 3.6) 按照得分、权重、数量的规则降序排序
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);
// 4) 先从缓存中获取
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 search recommendation by suggest, keyword = {}.", keywordsToSearch);
return suggestResult;
}
// 5) 调用ES执行搜索
SearchResult searchResult = searchCommonService.doSearch(ISearchConstants.INDEX_NAME_SUGGEST, searchParam);
if (searchResult == null) {
return null;
}
// 6) 构建结果加入缓存
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=recommendBySuggestIndex][srcQueryWord={}][keywordsToSearch={}][resultTerms={}][cost={}]",
srcQueryWord, keywordsToSearch, resultTerms, System.currentTimeMillis() - begin);
return suggestResult;
}
private QueryBuilder buildQueryBuilderByLimit(List<String> terms) {
FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(QueryBuilders.matchAllQuery());
// 给品类类型的关键词加分
functionScoreQueryBuilder.add(QueryBuilders.termQuery("type", Integer.valueOf(2)),
ScoreFunctionBuilders.weightFactorFunction(3));
// 按词出现的顺序加分
for (int i = 0; i < terms.size(); i++) {
functionScoreQueryBuilder.add(QueryBuilders.termQuery("standardKeyword", CharUtils.standardized(terms.get(i))),
ScoreFunctionBuilders.weightFactorFunction(terms.size() - i));
}
functionScoreQueryBuilder.boostMode(CombineFunction.SUM);
return functionScoreQueryBuilder;
}
private QueryBuilder buildQueryBuilder(String keywordsToSearch, Set<String> termSet) {
// 3.1) 对于suggest的multi-fields至少要有一个字段匹配到 匹配打分为常量1
MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(keywordsToSearch.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("1");
FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(QueryBuilders.constantScoreQuery(queryBuilder));
for (String term : termSet) {
// 3.2) 对于完全匹配Term的加1分
functionScoreQueryBuilder.add(QueryBuilders.termQuery("standardKeyword", CharUtils.standardized(term)),
ScoreFunctionBuilders.weightFactorFunction(1));
// 3.3) 对于匹配到一个Term的加2分
functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_ik", term),
ScoreFunctionBuilders.weightFactorFunction(2));
}
// 3.4) 处理性别相关的关键词
if (termSet.contains("男") && !termSet.contains("女")) {
functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_ik", "女"),
ScoreFunctionBuilders.weightFactorFunction(-5));
} else if (!termSet.contains("男") && termSet.contains("女")) {
functionScoreQueryBuilder.add(QueryBuilders.termQuery("keyword.keyword_ik", "男"),
ScoreFunctionBuilders.weightFactorFunction(-5));
} else if (!termSet.contains("男") && !termSet.contains("女")) {
functionScoreQueryBuilder.add(QueryBuilders.termsQuery("keyword.keyword_ik", Arrays.asList("男", "女")),
ScoreFunctionBuilders.weightFactorFunction(-2));
}
functionScoreQueryBuilder.boostMode(CombineFunction.SUM);
return functionScoreQueryBuilder;
}
private boolean containsProductInSearchResult(SearchApiResult searchResult) {
JSONObject dataMap = ((JSONObject) searchResult.getData());
return CollectionUtils.isNotEmpty((List) dataMap.get("product_list"));
}
/**
* 根据商品列表搜索结果聚合去关键词
* 当推荐词为3个时 品牌和品类最多各两个
*
* @param productList 商品列表搜索结果
* @return 关键词列表
*/
private String aggKeywordsByProductList(List<Map<String, Object>> productList) {
Map<String, Double> brandMap = new HashMap<>(productList.size());
Map<String, Double> sortNameMap = new HashMap<>(productList.size());
for (Map<String, Object> product : productList) {
String brandName = (String) product.get("brand_name");
if (StringUtils.isNotEmpty(brandName)) {
brandMap.compute(brandName, (k, v) -> v == null ? 1.0D : v + 1.0D);
}
String sortName = (String) product.get("small_sort_name");
if (StringUtils.isNotEmpty(sortName)) {
sortNameMap.compute(sortName, (k, v) -> v == null ? 1.0D : v + 1.0D);
}
}
// 分别获取前两个品牌、分类
List<String> sortNameList = com.yoho.search.base.utils.CollectionUtils.getSortedKeys(sortNameMap, false)
.stream().limit(SMART_SUGGESTION_TERM_COUNT - 1).collect(Collectors.toList());
List<String> brandNameList = com.yoho.search.base.utils.CollectionUtils.getSortedKeys(brandMap, false)
.stream().limit(SMART_SUGGESTION_TERM_COUNT - 1).collect(Collectors.toList());
List<String> keywords = new ArrayList<>();
keywords.addAll(sortNameList);
keywords.addAll(brandNameList);
return keywords.isEmpty() ? "" : keywords.stream().collect(Collectors.joining(" "));
}
/**
* 根据SKN到productindex索引获取搜索结果,然后聚合出推荐词列表。
*
* @param queryWord 搜索的关键词
* @return 推荐词列表
*/
private String aggKeywordsBySkns(String queryWord) {
String sknStr = queryWord.toLowerCase().trim();
if (sknStr.startsWith("skn")) {
sknStr = sknStr.substring("skn".length()).trim();
}
List<Integer> sknList = searchCommonHelper.getQuerySknList(sknStr);
if (CollectionUtils.isEmpty(sknList)) {
return null;
}
int size = sknList.size() > SMART_SUGGESTION_COUNT_LIMIT ? SMART_SUGGESTION_COUNT_LIMIT : sknList.size();
SearchParam productIndexSearchParam = new SearchParam();
productIndexSearchParam.setSize(size);
productIndexSearchParam.setFiter(QueryBuilders.termsQuery("productSkn", sknList));
// 缓存和列表的很难命中 所以单独自己调 而没有走ProductListServiceImpl的代码
final String indexName = ISearchConstants.INDEX_NAME_PRODUCT_INDEX;
JSONObject jsonObject = searchCacheService.getJSONObjectFromCache(indexName, productIndexSearchParam);
if (jsonObject != null) {
CACHE_MATCH_REQUEST.info("match cache for product list search recommendation by skns, sknList = {}.", sknList);
return jsonObject.getString("aggKeyword");
}
SearchResult productIndexSearchResult = searchCommonService.doSearch(indexName, productIndexSearchParam);
if (productIndexSearchResult == null) {
return null;
}
List<Map<String, Object>> productList = productIndexSearchResult.getResultList().stream().map(map -> searchServiceHelper.getProductMap(map)).collect(Collectors.toList());
String aggKeyword = aggKeywordsByProductList(productList);
jsonObject = new JSONObject();
jsonObject.put("aggKeyword", aggKeyword);
return aggKeyword;
}
private boolean isQuerySkn(String queryWord) {
String checkStr = queryWord.toLowerCase().trim();
if (checkStr.startsWith("skn")) {
checkStr = checkStr.substring("skn".length()).trim();
}
return searchCommonHelper.isQuerySkn(checkStr);
}
private JSONObject defaultSuggestRecommendation() {
JSONObject emptySuggestResult = new JSONObject();
emptySuggestResult.put("terms_suggestion", DEFAULT_SUGGEST_TIPS);
return emptySuggestResult;
}
/**
* 根据搜索的关键词到conversion索引查询关联的关键词。
* 该索引包含了爬虫提取的关键词、品牌的关键词定义和自定义的关键词。
*
* @param queryWord 搜索的关键词
* @return 推荐关键词列表
*/
private String getSuggestConversionDestBySource(String queryWord) {
long begin = System.currentTimeMillis();
logger.info("[func=getSuggestConversionDestBySource][query={}][begin={}]", queryWord, begin);
// 1) 先检查自定义的配置
if (CUSTOM_SUGGEST_CONVERSIONS.containsKey(queryWord.trim().toLowerCase())) {
return CUSTOM_SUGGEST_CONVERSIONS.get(queryWord.trim().toLowerCase());
}
// 2) 根据terms搜索构造搜索请求 要求完全匹配source
SearchParam searchParam = new SearchParam();
searchParam.setQuery(QueryBuilders.termQuery("source.source_keyword", queryWord.trim().toLowerCase()));
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 search recommendation by 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列表
String source = (String) searchResult.getResultList().get(0).get("source");
dest = (String) searchResult.getResultList().get(0).get("dest");
logger.info("[func=getSuggestConversionDestBySource][source={}][dest={}]", 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;
}
/**
* 根据关键词和时间查询关键词转换关系
*
* @param paramMap 查询参数
* @return 满足查询要求的转换关系
*/
@Override
public SearchApiResult suggestConversionList(Map<String, String> paramMap) {
try {
long begin = System.currentTimeMillis();
logger.info("[func=suggestConversionList][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));
}
if (StringUtils.isEmpty(queryWord)) {
// 没有传入query时按照时间降序排序
searchParam.setSortBuilders(Arrays.asList(SortBuilders.fieldSort("updateTime").order(SortOrder.DESC)));
}
// 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=suggestConversionList][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);
}
}
}
... ...
... ... @@ -2,6 +2,7 @@ package com.yoho.search.service.servicenew.impl;
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;
... ... @@ -12,11 +13,11 @@ 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.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.sort.SortBuilder;
... ... @@ -47,11 +48,11 @@ public class SuggestServiceImpl implements ISuggestService, ApplicationEventPubl
private static final String SUGGEST_PARAM_GLOBAL = "contain_global";
// 返回智能搜索词的数量
private static final int SMART_SUGGESTION_TERM_COUNT = 3;
private static final int VALID_STATUS = 1;
@Autowired
private SearchCommonService searchCommonService;
@Autowired
private SearchCacheService searchCacheService;
... ... @@ -74,12 +75,12 @@ public class SuggestServiceImpl implements ISuggestService, ApplicationEventPubl
return new SuggestApiResult().setCode(400).setMessage("关键字[query]参数为空!");
}
keyword = keyword.toLowerCase();
if (keyword.length() > 100) {
if (keyword.length() > 30) {
return new SuggestApiResult().setCode(400).setMessage("关键字[query]参数非法!");
}
// 2)构建查询参数
SearchParam searchParam = buildSearchParam(paramMap);
SearchParam searchParam = buildSuggestSearchParam(paramMap);
// 3)先从缓存中获取
final String indexName = ISearchConstants.INDEX_NAME_SUGGEST;
... ... @@ -98,7 +99,7 @@ public class SuggestServiceImpl implements ISuggestService, ApplicationEventPubl
if (CollectionUtils.isEmpty(searchResult.getResultList())) {
// 5) suggest 纠错
SearchResult newSearchResult = getNewSearchResult(paramMap);
SearchResult newSearchResult = getSpellCorrectResult(paramMap);
if (newSearchResult != null) {
searchResult = newSearchResult;
}
... ... @@ -125,7 +126,48 @@ public class SuggestServiceImpl implements ISuggestService, ApplicationEventPubl
}
}
private String getCountField(Map<String, String> paramMap) {
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";
... ... @@ -136,9 +178,9 @@ public class SuggestServiceImpl implements ISuggestService, ApplicationEventPubl
}
}
private SearchResult getNewSearchResult(Map<String, String> paramMap) {
private SearchResult getSpellCorrectResult(Map<String, String> paramMap) {
String keyword = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD).toLowerCase();
String newKeyword = getSuggestionKeyword(keyword);
String newKeyword = getSpellingCorrectKeyword(keyword);
if (StringUtils.isEmpty(newKeyword)) {
return null;
}
... ... @@ -149,7 +191,7 @@ public class SuggestServiceImpl implements ISuggestService, ApplicationEventPubl
newParamMap.putAll(paramMap);
newParamMap.put(SearchRequestParams.PARAM_SEARCH_KEYWORD, newKeyword);
SearchParam newSearchParam = buildSearchParam(newParamMap);
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);
... ... @@ -157,190 +199,104 @@ public class SuggestServiceImpl implements ISuggestService, ApplicationEventPubl
}
}
private SearchParam buildSearchParam(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 getSuggestionKeyword(String keyword) {
/**
* 到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).field("keyword").size(1));
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 && searchResult.getSuggestionMap() != null && searchResult.getSuggestionMap().get("keyword_suggestion") != null) {
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);
return option.getText().string();
spellingCorrentKeyword = option.getText().string();
}
}
}
return null;
jsonObject = new JSONObject();
jsonObject.put("spellingCorrentKeyword", spellingCorrentKeyword);
searchCacheService.addJSONObjectToCache(ISearchConstants.INDEX_NAME_SUGGEST, suggestSearchParam, jsonObject);
return spellingCorrentKeyword;
} catch (Exception e) {
logger.error("Get suggestion by keyword [" + keyword + "] failed!", e);
logger.error("Get spelling correct keyword by [" + keyword + "] failed!", e);
return null;
}
}
/**
* 根据query词获取term建议和phrase建议。
* 用于搜索结果数量太少或者无结果的时候给予用户的搜索建议。
* 根据关键词查询suggest索引
*
* @param paramMap 商品列表搜索参数
* @return 包括term建议和phrase建议。
* @param paramMap 查询参数
* @return 满足查询要求的建议词
*/
public JSONObject suggestByTerms(Map<String, String> paramMap) {
// 1) 对query进行判断 为空时不处理
String queryWord = paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD);
if (StringUtils.isEmpty(queryWord)) {
return null;
}
@Override
public SearchApiResult suggestList(Map<String, String> paramMap) {
try {
long begin = System.currentTimeMillis();
logger.info("[func=suggestByTerms][query={}][begin={}]", queryWord, begin);
logger.info("[func=suggestList][param={}][begin={}]", paramMap, begin);
// 2) 根据terms搜索构造搜索请求
SearchParam searchParam = buildSearchParamForTerms(paramMap);
String queryWord = paramMap.get("query");
int page = StringUtils.isBlank(paramMap.get("page")) ? 1 : Integer.parseInt(paramMap.get("page"));
if (page < 1) {
throw new IllegalArgumentException("分页参数不合法");
}
// 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;
// 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);
}
// 4) 调用ES执行搜索
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;
}
final List<String> resultTerms = new ArrayList<>();
if (CollectionUtils.isNotEmpty(searchResult.getResultList())) {
// 5.1) 从suggest获取的keyword列表
searchResult.getResultList().forEach(map -> {
resultTerms.add((String) map.get("keyword"));
});
} else {
// 5.2) 从conversion获取keyword列表
String dest = getTermsBySuggestConversion(queryWord);
if (StringUtils.isNotEmpty(dest)) {
int count = 0;
for (String keyword : dest.split(",")) {
if(!queryWord.equalsIgnoreCase(keyword)) {
count++;
resultTerms.add(keyword);
// 最多返回三个推荐词
if (count == SMART_SUGGESTION_TERM_COUNT) {
break;
}
}
}
}
}
// 6) 加入缓存
suggestResult = new JSONObject();
suggestResult.put("terms_suggestion", resultTerms);
searchCacheService.addJSONObjectToCache(indexName, searchParam, suggestResult);
logger.info("[func=suggestByTerms][query={}][cost={}]", queryWord, System.currentTimeMillis() - begin);
return suggestResult;
// 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("[func=suggestByTerms]Get suggestion by keyword [" + queryWord + "] failed!", e);
return null;
}
}
private String getTermsBySuggestConversion(String queryWord) {
long begin = System.currentTimeMillis();
logger.info("[func=getTermsBySuggestConversion][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);
// 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=getTermsBySuggestConversion][source={}][dest={}]", searchResult.getResultList().get(0).get("source"), dest);
logger.error(e.getMessage(), e);
return new SearchApiResult().setCode(500).setMessage(e.getMessage()).setData(null);
}
// 6) 加入缓存
suggestResult = new JSONObject();
suggestResult.put("dest", dest);
searchCacheService.addJSONObjectToCache(indexName, searchParam, suggestResult);
logger.info("[func=getTermsBySuggestConversion][query={}][cost={}]", queryWord, System.currentTimeMillis() - begin);
return dest;
}
private SearchParam buildSearchParamForTerms(Map<String, String> paramMap) {
final String countField = getCountField(paramMap);
SearchParam searchParam = new SearchParam();
MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(paramMap.get(SearchRequestParams.PARAM_SEARCH_KEYWORD));
queryBuilder.field("keyword").field("keyword.keyword_ik", 10F).field("keyword.keyword_pinyin").field("keyword.keyword_jianpin")
.analyzer("ik_smart")
.type(MultiMatchQueryBuilder.Type.BEST_FIELDS)
.operator(MatchQueryBuilder.Operator.OR)
.minimumShouldMatch("20%");
// 根据关联的数量增加打分 _score = _score * 0.05 * log(count + 2)
// FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(queryBuilder);
// functionScoreQueryBuilder.add(ScoreFunctionBuilders.fieldValueFactorFunction(countField).factor(0.05F).modifier(FieldValueFactorFunction.Modifier.LOG2P).missing(0));
searchParam.setQuery(queryBuilder);
searchParam.setPage(1);
searchParam.setSize(SMART_SUGGESTION_TERM_COUNT);
searchParam.setFiter(QueryBuilders.rangeQuery(countField).gte(20));
return searchParam;
}
}
... ...
... ... @@ -30,7 +30,7 @@ search.index.translog.flush_threshold_ops=5000
#search
search.minimum.should.match=75%
search.operator=and
search.default.field=brandName.brandName_lowercase^2500,smallSort^1000,smallSort.smallSort_pinyin^1000,middleSort^950,middleSort.middleSort_pinyin^950,maxSort^900,maxSort.maxSort_pinyin^900,brandName^900,brandNameCn^850,brandNameCn.brandNameCn_pinyin^850,brandNameEn^800,brandDomain^800,specialSearchField^700,productName.productName_ansj^300,standardOnlyNames.standardOnlyNames_pinyin^250,standardOnlyNames.standardOnlyNames_ansj^250,productKeyword^50,brandKeyword^30,genderS^20,salesPhrase^50,marketPhrase^50,searchField_ansj^10,searchField,productSkn.productSkn_ansj
search.default.field=brandName.brandName_lowercase^2500,smallSort^1000,smallSort.smallSort_pinyin^1000,middleSort^950,middleSort.middleSort_pinyin^950,maxSort^900,maxSort.maxSort_pinyin^900,brandName^900,brandNameCn^850,brandNameCn.brandNameCn_pinyin^850,brandNameEn^800,brandDomain^800,specialSearchField^700,productName.productName_ansj^300,standardOnlyNames.standardOnlyNames_pinyin^250,standardOnlyNames.standardOnlyNames_ansj^250,productKeyword^50,brandKeyword^30,genderS^20,salesPhrase^50,marketPhrase^50,searchField_ansj^10,searchField,productSkn.productSkn_ansj,productSkn.productSkn_ik
search.script.score=_score+doc['sortWeight'].value*0.003+(100-doc['breakingRate'].value)/100 * doc['salesWithDateDiff'].value/pow((now-doc['shelveTime'].value)/3600+2,1.8)
search.script.lang=groovy
... ...
... ... @@ -32,4 +32,5 @@ search.price.plan.open=true
search.dynamic.rule.open=false
#better search suggestion
search.better.suggestion.open=true
\ No newline at end of file
search.suggestion.tips.open=true
search.suggestion.tips.conversion.open=true
\ No newline at end of file
... ...
... ... @@ -30,7 +30,7 @@ search.index.translog.flush_threshold_ops=${search.index.translog.flush_threshol
#search
search.minimum.should.match=75%
search.operator=and
search.default.field=brandName.brandName_lowercase^2500,smallSort^1000,smallSort.smallSort_pinyin^1000,middleSort^950,middleSort.middleSort_pinyin^950,maxSort^900,maxSort.maxSort_pinyin^900,brandName^900,brandNameCn^850,brandNameCn.brandNameCn_pinyin^850,brandNameEn^800,brandDomain^800,specialSearchField^700,productName.productName_ansj^300,standardOnlyNames.standardOnlyNames_pinyin^250,standardOnlyNames.standardOnlyNames_ansj^250,productKeyword^50,brandKeyword^30,genderS^20,salesPhrase^50,marketPhrase^50,searchField_ansj^10,searchField,productSkn.productSkn_ansj
search.default.field=brandName.brandName_lowercase^2500,smallSort^1000,smallSort.smallSort_pinyin^1000,middleSort^950,middleSort.middleSort_pinyin^950,maxSort^900,maxSort.maxSort_pinyin^900,brandName^900,brandNameCn^850,brandNameCn.brandNameCn_pinyin^850,brandNameEn^800,brandDomain^800,specialSearchField^700,productName.productName_ansj^300,standardOnlyNames.standardOnlyNames_pinyin^250,standardOnlyNames.standardOnlyNames_ansj^250,productKeyword^50,brandKeyword^30,genderS^20,salesPhrase^50,marketPhrase^50,searchField_ansj^10,searchField,productSkn.productSkn_ansj,productSkn.productSkn_ik
search.script.score=_score+doc['sortWeight'].value*0.003+(100-doc['breakingRate'].value)/100 * doc['salesWithDateDiff'].value/pow((now-doc['shelveTime'].value)/3600+2,1.8)
search.script.lang=groovy
... ...