实现得分的PrefixQuery

本文介绍如何在 Lucene 中实现一个可以计算得分的 PrefixQuery。通过重写 MultiTermQuery 的重写方法,使用 SCORING_BOOLEAN_QUERY_REWRITE 以确保搜索结果能够根据相关性进行排序。此外,还调整了 BooleanQuery 的最大子查询数量限制,以避免 TooManyClauses 异常。

(先声明一下,我使用的lucene的版本是lucene4.7.2)

在lucene中,有一种类型的query叫做MultiTermQuery,故名思议,他是要涉及到很多个term的query,比如我们常用的WildcardQuery、FuzzyQuery、PrefixQuery、TermRangeQuery、NumericRangeQuery等,他们都是需要按照一个或者多个term按照一定的逻辑找到多个term,然后再重写由找到的这些term形成的TermQuery进入一个新的Query(比如BooleanQuery、或者ConstantScoreQuery),但是有个一指的注意的地方是:有些MultiTermQuery是不得分的,也就是在返回的时候不会按照得分排序,比如PrefixQuery,的不得分是由每个MultiTermQuery使用的rewriteMethod指定,也就是由重写规则指定。本文的目的不在于讨论重写规则,而是想实现一个可以得分的PrefixQuery(业务场景是我们要使用PrefixQuery做搜索框中提示词的排序,所以必须实现得分)。

 

实现原理很简单,在指定重写规则的时候将重写规则指定为得分的规则(当然这里涉及到重写规则的实现,这里本文不讨论),在org.apache.lucene.search.MultiTermQuery类中含有SCORING_BOOLEAN_QUERY_REWRITE这个重写规则从他的名字中就可以理解是封装为一个BooleanQuery,并且计算分数。他的逻辑很简单,将搜索到的多个termQuery封装成一个booleanQuery,每一个termQuery都是optional的,也就是对多个termQuery取并集。但是Booleanquery有个需要注意的地方,他不能有太多的clause,不然会报错,默认是1024个,所以我们需要修改这个值,做到这里就算是完成了。我的代码如下:

/**
 * 由于solr自带的PrefixQuery是不得分的,不能满足提示词的排序要求,所以重写这个query.
 */
public class ScoredPrefixQuery extends PrefixQuery {

	//从词典表中得到的term的限制,用于做测试的,实际中不用
	private int limit = -1; 
	
	static{
		BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);//设置BooleanQuery的最多的子query的个数为Integer.MAX_VALUE。
	}
	
	public ScoredPrefixQuery(Term prefix) {
		super(prefix);
		//重置重写规则,使用得分的booleanQuery,此处存在的问题是可能会发生BooleanQuery.TooManyClauses,所以要在得到term的时候需要做限制
		setRewriteMethod(org.apache.lucene.search.MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE);
	}
	/**
         * 从词典表中得到前缀匹配的term的方法
         */
	@Override
	public TermsEnum getTermsEnum(Terms terms, AttributeSource atts) throws IOException {
		 TermsEnum tenum = terms.iterator(null);
		 if (getPrefix().bytes().length == 0) {
		      // no prefix -- match all terms for this field:
		     return tenum;
		 }
		 return new PrefixTermsEnum(tenum, getPrefix().bytes()) {
			 int already = 0;
			 final int termLimit =  limit==-1?BooleanQuery.getMaxClauseCount():limit;//设置limit只是用于做测试的。
			 
			 @Override
			public BytesRef next() throws IOException {
				
				BytesRef ref = super.next();//先调用父类方法,即从词典表中读取,
				if(ref == null){//如果真的读完了,就返回null。
					return null;
				}else{//没有读取完,则判断是否已经读取了太多的term
					//最多的BooleanClause的个数
					if(already++ < termLimit){//一个前缀最多从词典表中得到booleanquery的MaxClause个,这样就不会报错了。
						return ref;
					}
					return null;
				}
			}
		 };
	}
	
	public int getLimit() {
		return limit;
	}
	
	//做测试用的
	public void setLimit(int limit) {
		this.limit = limit;
	}
	
//这个测试的前提是我们在索引中仅仅保存了只有id域的100个document,id为从0-99,省略了建立索引的代码。
	public static void main(String[] args) throws IOException {
		
		IndexReader reader = DirectoryReader.open(getDirectory());
		
		IndexSearcher search = new IndexSearcher(reader);
		ScoredPrefixQuery q = new ScoredPrefixQuery(new Term("id","1"));//这一行和下面的PrefixQuery q 这一行是区分的,如果使用这一行则只会搜到3个,并且得分不是1.0f,也就是是得分的。
		q.setLimit(3);//设置最多为3个。
		
//		PrefixQuery q = new PrefixQuery(new Term("id", "1"));//如果使用lucene中默认使用的PrefixQuery则会搜到11个,并且得分都是1.0f,也就是没有得分。
		
		TopDocs td = search.search(q, 100);
		for(ScoreDoc sd:td.scoreDocs){
			System.out.println(sd.score);
		}
		System.out.println(td.scoreDocs.length);
		
	}	
}

 

这样就完成了得分的前缀匹配的query,如果要在solr中使用,还需要自己定义queryparser的插件,这个留在以后再写博客。

 

 

 

@Verify @Override public ResponseVo<List<GoodsSpuPageEsVo>> pageEs(EsPageParamPo pageParam, int page, int size) { if (page < 1) { throw new OSPException(OSPError.PARAM_ERROR, "page必须大于1"); } if (size < 1 || size > 500) { throw new OSPException(OSPError.PARAM_ERROR, "size范围1~100"); } //Integer cusOrgId1 = sysConfigService.getName("SET_DEFAULT_USER_ENTERPRISE_INFO", Integer.class); Pageable pageable = Pageable.newInstance(page, size, pageParam); CustomerEnterprise customerEnterprise = null; String cusEsPathPrefix = null; if (null != pageParam.getCusOrgId()) { customerEnterprise = customerEnterpriseMapper.selectById(pageParam.getCusOrgId()); if (customerEnterprise == null) { log.error("商品使用客群属性es搜索,客群id不存在 {}", pageParam.getCusOrgId()); return ResponseVo.empty(pageable); } cusEsPathPrefix = "cusInfo." + customerEnterprise.getId() + "."; } List<String> cusEsPathPrefixs = new ArrayList<>(); if (CollectionUtils.isNotEmpty(pageParam.getCusOrgIds())) { List<CustomerEnterprise> customerEnterprises = customerEnterpriseMapper.selectList(MCustomerEnterprise.in(MCustomerEnterprise.id, pageParam.getCusOrgIds())); if (CollectionUtils.isEmpty(customerEnterprises)) { log.error("商品使用客群属性es搜索,客群id不存在 {}", pageParam.getCusOrgId()); return ResponseVo.empty(pageable); } customerEnterprises.forEach(enterprise -> { cusEsPathPrefixs.add("cusInfo." + enterprise.getId() + "."); }); } String indexName = sysConfigService.getName(Constants.ES_GOODS_TO_ELASTICSEARCH_INDEX_KEY, String.class, Constants.ES_GOODS_TO_ELASTICSEARCH_INDEX_DEFUALT_VALUE); String minimumShouldMatch = sysConfigService.getName(Constants.ES_MINIMUM_SHOULD_MATCH_KEY, String.class, Constants.ES_MINIMUM_SHOULD_MATCH_DEFUALT_VALUE); HighlightBuilder highlightBuilder = new HighlightBuilder(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); if (StringUtils.isNotBlank(pageParam.getGoodsName())) { boolQuery.must(QueryBuilders.multiMatchQuery(pageParam.getGoodsName(), "author", "masterName", "publishYear", "searchTags", "name", "typeName", "publishingHouse") .field("name", 2.0f).operator(Operator.OR).minimumShouldMatch(minimumShouldMatch)); // 配置高亮显示 highlightBuilder.field("name"); highlightBuilder.field("author"); // 设置高亮标签 highlightBuilder.preTags("<span style=\"color:#FF0000;\">"); highlightBuilder.postTags("</span>"); /** 相似度搜索,提高全文搜索的_score评分 */ boolQuery.should(QueryBuilders.matchPhraseQuery("name", pageParam.getGoodsName()).slop(6)); boolQuery.should(QueryBuilders.matchPhraseQuery("isbn", pageParam.getGoodsName()).slop(4)); boolQuery.should(QueryBuilders.matchPhraseQuery("author", pageParam.getGoodsName()).slop(2)); boolQuery.should(QueryBuilders.matchPhraseQuery("searchTags", pageParam.getGoodsName()).slop(2)); boolQuery.should(QueryBuilders.matchPhraseQuery("typeName", pageParam.getGoodsName()).slop(2)); boolQuery.should(QueryBuilders.matchPhraseQuery("masterName", pageParam.getGoodsName()).slop(3)); boolQuery.should(QueryBuilders.matchPhraseQuery("publishYear", pageParam.getGoodsName()).slop(3)); boolQuery.should(QueryBuilders.matchPhraseQuery("publishingHouse", pageParam.getGoodsName()).slop(3)); // 设置minimum_should_match参数 boolQuery.minimumShouldMatch(1); } if (StringUtils.isNotBlank(pageParam.getSearchInfo())) { boolQuery.must(QueryBuilders.multiMatchQuery(pageParam.getSearchInfo(), "name", "isbn", "spuId") //TODO 这里重设了 字段的得分权重 .field("name", (float) 2.0).operator(Operator.OR).minimumShouldMatch(minimumShouldMatch)); highlightBuilder.field("name"); /**相似度搜索,提高全文搜索的_score的评分*/ boolQuery.should(QueryBuilders.matchPhraseQuery("name", pageParam.getSearchInfo()).slop(6)); boolQuery.should(QueryBuilders.matchPhraseQuery("isbn", pageParam.getSearchInfo()).slop(2)); boolQuery.should(QueryBuilders.matchPhraseQuery("spuId", pageParam.getSearchInfo()).slop(2)); } if (StringUtils.isNotBlank(pageParam.getLeftName())) { // 配置高亮显示 highlightBuilder.field("name"); // 设置高亮标签 highlightBuilder.preTags("<span style=\"color:#FF0000;\">"); highlightBuilder.postTags("</span>"); boolQuery.must(QueryBuilders.matchQuery("name", pageParam.getLeftName())); // 可选:添加模糊匹配提高召回率 //boolQuery.should(QueryBuilders.matchQuery("name", pageParam.getLeftName())); // 设置minimum_should_match //boolQuery.minimumShouldMatch(0); } String productClassCode = pageParam.getProductClassCode(); if (customerEnterprise != null) { boolQuery.must(QueryBuilders.termQuery(cusEsPathPrefix + "canGoods", "true")); boolQuery.must(QueryBuilders.termQuery(cusEsPathPrefix + "canSupplierCode", "true")); if (!productClassCode.equals(ProductClassCodeEnum.VIRTUAL.code)) { boolQuery.must(QueryBuilders.termQuery(cusEsPathPrefix + "canType", "true")); } } if (CollectionUtils.isNotEmpty(cusEsPathPrefixs)) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); cusEsPathPrefixs.forEach(prefix -> { // 为每个前缀创建一个必须满足所有条件的布尔查询 BoolQueryBuilder prefixQuery = QueryBuilders.boolQuery(); prefixQuery.must(QueryBuilders.termQuery(prefix + "canGoods", "true")); prefixQuery.must(QueryBuilders.termQuery(prefix + "canSupplierCode", "true")); if (!productClassCode.equals(ProductClassCodeEnum.VIRTUAL.code)) { prefixQuery.must(QueryBuilders.termQuery(prefix + "canType", "true")); } // 将每个前缀的查询添加到主查询中作为 should 条件 subBoolQuery.should(prefixQuery); }); // 设置 minimum_should_match 为 1,表示只需满足任意一个前缀的所有条件即可 subBoolQuery.minimumShouldMatch(1); boolQuery.must(subBoolQuery); } if (null != pageParam.getLowPrice() || null != pageParam.getHighPrice()) { RangeQueryBuilder rangeQueryBuilder = null; if (customerEnterprise != null) { rangeQueryBuilder = QueryBuilders.rangeQuery(cusEsPathPrefix + "price"); } else { rangeQueryBuilder = QueryBuilders.rangeQuery("sellPrice"); } if (null != pageParam.getLowPrice()) { rangeQueryBuilder.gte(pageParam.getLowPrice()); } if (null != pageParam.getHighPrice()) { rangeQueryBuilder.lte(pageParam.getHighPrice()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getCreateTimeStart() || null != pageParam.getCreateTimeEnd()) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("createTimeMillis"); if (null != pageParam.getCreateTimeStart()) { rangeQueryBuilder.gte(pageParam.getCreateTimeStart()); } if (null != pageParam.getCreateTimeEnd()) { rangeQueryBuilder.lte(pageParam.getCreateTimeEnd()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getUpdateTimeStart() || null != pageParam.getUpdateTimeEnd()) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("updateTimeMillis"); if (null != pageParam.getUpdateTimeStart()) { rangeQueryBuilder.gte(pageParam.getUpdateTimeStart()); } if (null != pageParam.getUpdateTimeEnd()) { rangeQueryBuilder.lte(pageParam.getUpdateTimeEnd()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getUpTimeStart() || null != pageParam.getUpTimeEnd()) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("upTimeMillis"); if (null != pageParam.getUpTimeStart()) { rangeQueryBuilder.gte(pageParam.getUpTimeStart()); } if (null != pageParam.getUpTimeEnd()) { rangeQueryBuilder.lte(pageParam.getUpTimeEnd()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getStockStart() || null != pageParam.getStockEnd()) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("stock"); if (null != pageParam.getStockStart()) { rangeQueryBuilder.gte(pageParam.getStockStart()); } if (null != pageParam.getStockEnd()) { rangeQueryBuilder.lte(pageParam.getStockEnd()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getOnTop()) { boolQuery.must(QueryBuilders.termQuery("onTop", pageParam.getOnTop())); } if (null != pageParam.getSupplierCodeList() && !pageParam.getSupplierCodeList().isEmpty()) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); pageParam.getSupplierCodeList().forEach(one -> { subBoolQuery.should(QueryBuilders.termQuery("supplierCode", one)); }); boolQuery.must(subBoolQuery); } if (null != pageParam.getProductClassCodeList() && !pageParam.getProductClassCodeList().isEmpty()) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); pageParam.getProductClassCodeList().forEach(one -> { subBoolQuery.should(QueryBuilders.termQuery("productClassCode.keyword", one)); }); boolQuery.must(subBoolQuery); } if (StringUtils.isNotBlank(pageParam.getProductClassCode())) { boolQuery.must(QueryBuilders.termQuery("productClassCode.keyword", pageParam.getProductClassCode())); // boolQuery.must(QueryBuilders.termQuery("label.count", pageParam.getProductClassCode())); }/* else { boolQuery.must(QueryBuilders.termQuery("productClassCode.keyword", ProductClassCodeEnum.BOOK.code)); }*/ if (null != pageParam.getGoodsTypeIdList() && !pageParam.getGoodsTypeIdList().isEmpty()) { MGoodsType.in(MGoodsType.code, pageParam.getGoodsTypeIdList()).eq(MGoodsType.onDel, DeleteEnum.NO_DELETE.code).eq(MGoodsType.channelId, "3106").eq(MGoodsType.status, YESOrNOEnum.STATUS_1.code); if (StringUtils.isNotBlank(pageParam.getProductClassCode())) { MGoodsType.eq(MGoodsType.productClassCode, pageParam.getProductClassCode()); } else { MGoodsType.eq(MGoodsType.productClassCode, ProductClassCodeEnum.BOOK.code); } List<GoodsType> typeList = goodsTypeMapper.selectList(MGoodsType.build()); Stream<String> typePathList = typeList.stream().map(GoodsType::getPath); BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); typePathList.forEach(one -> { subBoolQuery.should(QueryBuilders.prefixQuery("typePath", one)); }); boolQuery.must(subBoolQuery); } if (null != pageParam.getSpuIds() && !pageParam.getSpuIds().isEmpty()) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); pageParam.getSpuIds().forEach(one -> { subBoolQuery.should(QueryBuilders.termQuery("spuId", one)); }); boolQuery.must(subBoolQuery); } if (CollectionUtils.isNotEmpty(pageParam.getBanSpuIds())) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); pageParam.getBanSpuIds().forEach(one -> { subBoolQuery.should(QueryBuilders.termQuery("spuId", one)); }); boolQuery.mustNot(subBoolQuery); } if (null != pageParam.getTags() && !pageParam.getTags().isEmpty()) { // boolQuery.must(QueryBuilders.matchQuery(channelKey + "tag", pageParam.getTags().stream().collect(Collectors.joining(" "))).minimumShouldMatch("60%")); Map<String, Object> params = Maps.newHashMap(); String scriptStr = ""; for (int i = 0; i < pageParam.getTags().size(); i++) { params.put("to_tag" + i, pageParam.getTags().get(i)); scriptStr += "doc['tags.keyword'].values.contains(params.to_tag" + i + ") "; if (i != (pageParam.getTags().size() - 1)) { scriptStr += "|| "; } } Script script = new Script(Script.DEFAULT_SCRIPT_TYPE, Script.DEFAULT_SCRIPT_LANG, scriptStr, params); boolQuery.must(QueryBuilders.scriptQuery(script)); } if (null != pageParam.getStatus()) { boolQuery.must(QueryBuilders.termQuery("status", pageParam.getStatus())); } if (null != pageParam.getMasterStatus()) { boolQuery.must(QueryBuilders.termQuery("masterStatus", pageParam.getMasterStatus())); } boolQuery.must(QueryBuilders.termQuery("onDel", DeleteEnum.NO_DELETE.code)); boolQuery.must(QueryBuilders.termQuery("masterOnDel", DeleteEnum.NO_DELETE.code)); searchSourceBuilder.query(boolQuery); if (!highlightBuilder.fields().isEmpty()) { searchSourceBuilder.highlighter(highlightBuilder); } searchSourceBuilder.from((page - 1) * size); searchSourceBuilder.size(size); //随机分页查询 放在排序前,否则无效 if (null != pageParam.getIsOpenRandomPage() && pageParam.getIsOpenRandomPage().equals(1)) { Script script = new Script("Math.random()"); ScriptSortBuilder sortBuilder = new ScriptSortBuilder(script, ScriptSortBuilder.ScriptSortType.NUMBER); searchSourceBuilder.sort(sortBuilder); } /**是否默认排序*/ boolean defaultSort = true; if (null != pageParam.getFixedPriceSort()) { defaultSort = false; searchSourceBuilder.sort("fixedPrice", SortTypeEnum.TYPE_1.code.equals(pageParam.getFixedPriceSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getUpTimeSort()) { defaultSort = false; searchSourceBuilder.sort("upTimeMillis", SortTypeEnum.TYPE_1.code.equals(pageParam.getUpTimeSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getCreateSort()) { defaultSort = false; searchSourceBuilder.sort("createTimeMillis", SortTypeEnum.TYPE_1.code.equals(pageParam.getCreateSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getUpdateSort()) { defaultSort = false; searchSourceBuilder.sort("updateTimeMillis", SortTypeEnum.TYPE_1.code.equals(pageParam.getUpdateSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getRecommendSort()) { defaultSort = false; Map<String, Object> params = Maps.newHashMap(); params.put("to_tag", "推荐"); // Script script=new Script(Script.DEFAULT_SCRIPT_TYPE,Script.DEFAULT_SCRIPT_LANG,"doc['"+channelKey+"tag.keyword'].value==params.to_tag?1:0",params); /**同时存在多个值时候,原来的== 判断就不起作用*/ Script script = new Script(Script.DEFAULT_SCRIPT_TYPE, Script.DEFAULT_SCRIPT_LANG, "doc['tags.keyword'].values.contains(params.to_tag)?1:0", params); ScriptSortBuilder scriptSortBuilder = SortBuilders.scriptSort(script, ScriptSortBuilder.ScriptSortType.NUMBER).order(SortTypeEnum.TYPE_1.code.equals(pageParam.getRecommendSort()) ? SortOrder.ASC : SortOrder.DESC); searchSourceBuilder.sort(scriptSortBuilder); searchSourceBuilder.sort(SortBuilders.fieldSort("onTop").order(SortOrder.DESC).unmappedType("integer")); searchSourceBuilder.sort(SortBuilders.fieldSort("sort").order(SortOrder.ASC).unmappedType("integer")); } if (null != pageParam.getPriceSort()) { defaultSort = false; if (customerEnterprise != null) { searchSourceBuilder.sort(cusEsPathPrefix + "price", SortTypeEnum.TYPE_1.code.equals(pageParam.getPriceSort()) ? SortOrder.ASC : SortOrder.DESC); } else { searchSourceBuilder.sort("sellPrice", SortTypeEnum.TYPE_1.code.equals(pageParam.getPriceSort()) ? SortOrder.ASC : SortOrder.DESC); } } if (null != pageParam.getSaleCntSort()) { defaultSort = false; searchSourceBuilder.sort("spuSaleCnt", SortTypeEnum.TYPE_1.code.equals(pageParam.getSaleCntSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getPublishTimeSort()) { // 根据出版时间排序 defaultSort = false; FieldSortBuilder fieldSortBuilder = SortBuilders.fieldSort("publishTimeMillis").order(SortTypeEnum.TYPE_1.code.equals(pageParam.getPublishTimeSort()) ? SortOrder.ASC : SortOrder.DESC).unmappedType("number"); searchSourceBuilder.sort(fieldSortBuilder); } if (StringUtils.isNotBlank(pageParam.getLeftName())) { searchSourceBuilder.sort("_score", SortOrder.DESC); } else { if (defaultSort) { if (StringUtils.isNotBlank(pageParam.getGoodsName())) { searchSourceBuilder.sort("_score", SortOrder.DESC); } /**当字段不存在的时候,排序需要指定 默认的字段数据类型 否则会报错*/ searchSourceBuilder.sort(SortBuilders.fieldSort("onTop").order(SortOrder.DESC).unmappedType("integer")); searchSourceBuilder.sort(SortBuilders.fieldSort("sort").order(SortOrder.ASC).unmappedType("integer")); } } log.info("搜索查询语句 index:{}:\n{}", indexName, searchSourceBuilder.toString()); Search search = new Search.Builder(searchSourceBuilder.toString()).addIndex(indexName).addType(Constants.ES_TYPE_NAME).build(); try { SearchResult jr = null; jr = jestClient.execute(search); if (log.isDebugEnabled()) { log.debug("搜索查询返回:{}", jr == null ? "" : jr.getJsonString()); } List<SearchResult.Hit<NEsGoodsSpuVo, Void>> hits = jr.getHits(NEsGoodsSpuVo.class); log.info("搜索查询返回数据条数 ----> {}", hits.size()); List<GoodsSpuPageEsVo> goodsSpuPageEsVos = new ArrayList<>(); hits.forEach(hit -> { GoodsSpuPageEsVo goodsSpuPageEsVo = BeanUtil.copyProperties(hit.source, GoodsSpuPageEsVo.class); if (hit.highlight != null && !hit.highlight.isEmpty()) { hit.highlight.entrySet().forEach(highlight -> { if (highlight.getKey().equals("name") && !highlight.getValue().isEmpty()) { goodsSpuPageEsVo.setHighlightName(highlight.getValue().get(0)); } if (highlight.getKey().equals("author") && !highlight.getValue().isEmpty()) { goodsSpuPageEsVo.setHighlightAuthor(highlight.getValue().get(0)); } }); } goodsSpuPageEsVos.add(goodsSpuPageEsVo); }); pageable.setTotal(jr.getTotal()); return ResponseVo.sucess(goodsSpuPageEsVos, pageable); } catch (IOException e) { log.error("查询es异常", e); throw new OSPException(OSPError.RUNTIME_ERROR, "商品搜索引擎异常"); } }价格区间,营销分类类型编码,推荐标签,推荐关键词
最新发布
08-07
<think>好的,用户想了解Elasticsearch中的`prefix`查询的详细情况。首先,我需要回忆一下`prefix`查询的基本概念和用途。记得`prefix`查询主要用于查找某个字段中以指定前缀开头的文档,类似于SQL中的`LIKE 'prefix%'`。不过,Elasticsearch处理文本的方式可能有所不同,特别是涉及到分析器(analyzer)和倒排索引的结构。 接下来,我应该考虑`prefix`查询的工作原理。它应该是如何利用倒排索引来快速查找匹配前缀呢?可能需要提到倒排索引中的词项是按字典序排列的,这样前缀查询可以高效地扫描符合条件的词项。但要注意,如果字段是 analyzed 的,可能会被分词成多个词项,这时候`prefix`查询可能不会按预期工作,因为每个分词后的词项单独处理。例如,如果原文本是“quick brown fox”,分词后可能变成["quick", "brown", "fox"],这时候用`prefix`查询“qui”会匹配到“quick”,而“bro”则会匹配到“brown”。 然后,需要讨论`prefix`查询的性能问题。因为前缀查询可能需要扫描很多词项,特别是在字段有高基数的情况下,比如用户ID或邮箱前缀,这可能导致查询变慢。为了优化性能,可能需要使用`index_prefixes`参数,这样Elasticsearch会在索引时预先为特定长度的前缀建立索引,加快查询速度。 还需要比较`prefix`查询和`wildcard`查询的区别。虽然两者都用于模式匹配,但`wildcard`支持通配符,如`?`和`*`,但性能可能更差,尤其是在通配符出现在开头的时候,比如`*prefix`,这时候需要遍历所有词项。而`prefix`查询因为是前缀,可能在优化后更高效。 另外,需要注意大小写的问题。如果字段在索引时被标准化(如小写化处理),那么查询的前缀也应该使用小写,否则可能无法匹配。例如,如果原文本是“Apple”,经过小写处理变成“apple”,那么用前缀“App”可能无法匹配,但“app”可以。 可能还需要提到`prefix`查询的语法结构,如何在Elasticsearch的查询DSL中使用它,比如: ```json { "query": { "prefix": { "field_name": { "value": "prefix_text" } } } } ``` 或者更简洁的形式: ```json { "query": { "prefix": { "field_name": "prefix_text" } } } ``` 此外,是否需要结合其他查询,比如`bool`查询中的`must`或`filter`上下文,来组合多个条件? 可能还需要举一个实际的例子,比如在日志系统中查找以某个特定错误码开头的日志条目,或者电商中搜索以特定品牌前缀开头的商品名称。 还要注意,`prefix`查询默认是模糊的,是否支持模糊匹配?或者是否需要使用`fuzzy`查询来处理拼写错误的情况?这时候可能需要澄清,`prefix`查询本身不支持模糊,它严格匹配前缀,而如果有拼写错误的需求,可能需要结合其他查询类型。 最后,是否需要提醒用户在使用`prefix`查询时注意性能影响,特别是在大数据集下,以及如何通过索引优化(如`index_prefixes`)来提升性能? 总结一下,我需要分步骤解释`prefix`查询的作用、使用方式、性能考虑、与其他查询的区别,以及实际应用中的例子和最佳实践。同时,要确保语言通俗易懂,结构清晰,帮助用户全面理解并正确使用该查询类型。</think>以下是对Elasticsearch中`prefix`查询的详细说明: --- ### 一、什么是`prefix`查询? `prefix`查询用于查找**字段值以指定前缀开头**的文档。 类似于SQL中的`LIKE 'prefix%'`,但底层通过倒排索引实现高效匹配。 --- ### 二、基础语法 ```json GET /index/_search { "query": { "prefix": { "field_name": { "value": "prefix_text" } } } } ``` 或简化写法: ```json { "query": { "prefix": { "field_name": "prefix_text" } } } ``` --- ### 三、关键特性 #### 1. 分词影响 - 若字段是`text`类型(默认会被分词): 匹配的是分词后**单个词项**的前缀 *示例:* `"apple banana"` 分词为 `["apple", "banana"]` `prefix: "app"` → 匹配 `apple` `prefix: "ban"` → 匹配 `banana` - 若字段是`keyword`类型(不分词): 匹配整个字段值的前缀 *示例:* `"apple banana"` 作为整体 `prefix: "app"` → 匹配成功 `prefix: "apple b"` → 匹配成功 #### 2. 大小写敏感 依赖字段的`analyzer`设置: - 若使用`lowercase`分析器:索引和查询的文本都会被转为小写 *建议:统一使用小写进行前缀匹配* #### 3. 性能优化 默认情况下,`prefix`查询需要扫描倒排索引中的词项列表。 **优化方案:** 启用`index_prefixes`参数(需在mapping中预先配置): ```json { "mappings": { "properties": { "field_name": { "type": "text", "index_prefixes": { "min_chars": 2, // 最小前缀长度 "max_chars": 5 // 最大前缀长度 } } } } } ``` Elasticsearch会为`2-5`长度的前缀单独建立索引,显著提升查询速度。 --- ### 四、与其他查询对比 | 查询类型 | 特点 | 性能 | |----------------|----------------------------------------------------------------------|------------| | `prefix` | 仅匹配前缀,不支持通配符 | 较高 | | `wildcard` | 支持`*`(任意字符)和`?`(单个字符),但前导通配符(如`*fix`)性能极差 | 低 | | `match_phrase` | 精确短语匹配,不适用前缀场景 | 中等 | --- ### 五、实际应用场景 #### 1. 日志搜索 查找以`ERR-100`开头的错误码: ```json { "query": { "prefix": { "error_code": "ERR-100" } } } ``` #### 2. 商品搜索 匹配品牌名以`NIKE`开头的商品: ```json { "query": { "prefix": { "brand.keyword": "NIKE" } } } ``` #### 3. 自动补全 结合`index_prefixes`实现快速前缀提示(需提前配置映射)。 --- ### 六、注意事项 1. **避免长前缀**:过长的前缀会降低查询效率 2. **慎用`text`类型字段**:建议对需要精确前缀匹配的字段使用`keyword`类型 3. **结合`filter`上下文**:若不需要相关性算分,用`filter`可缓存结果提升性能 ```json { "query": { "bool": { "filter": [ { "prefix": { "field": "text" } } ] } } } ``` --- 通过合理使用`prefix`查询,可以在Elasticsearch中高效实现前缀匹配需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值