如何在Elasticsearch语义搜索中添加分面、过滤与权重提升
问题描述
已实现基于OpenAI、SpringBoot、Vaadin的Elasticsearch语义搜索并运行正常,现需为查询添加facet(分面)、**filter(过滤)和boost(权重提升)**功能。
Post实体类
@Document( indexName="posts" ) @ToString public class Post { @Id private String id; @Field(type = FieldType.Text) private String title; @Field(type = FieldType.Text) private String content; @Field(type = FieldType.Text) private String color;// e.g. red, blue, green @Field(type = FieldType.Text) private String size;//e.g XS,S,M,L,XL,XXL @Field(type = FieldType.Dense_Vector, dims = 1536, index = true) private Vector<Double> embedding; public Post(String title, String content) { this.title = title; this.content = content; } }
当前基础查询代码
@Repository public interface PostRepository extends ElasticsearchRepository<Post, String>{ @Query("{ " + " \"script_score\": { " + " \"query\": {\"match_all\": {} }, " + " \"script\": { "+ " \"source\": \"cosineSimilarity(params.queryVector, 'embedding') +0.1\", "+ " \"params\": {\"queryVector\": ?0 } "+ " } "+ " } "+ "}") List<Post> findBySimilar( String content); }
正确实现方案
1. 过滤(Filter)功能
前置优化:字段类型调整
color和size是离散枚举值,当前Text类型会被分词,不适合精确匹配。建议修改为Keyword类型:
@Field(type = FieldType.Keyword) private String color;// e.g. red, blue, green @Field(type = FieldType.Keyword) private String size;//e.g XS,S,M,L,XL,XXL
动态过滤查询实现
支持动态传入过滤参数,将过滤条件嵌入bool查询的filter节点(不影响评分):
@Repository public interface PostRepository extends ElasticsearchRepository<Post, String> { @Query("{ " + " \"bool\": { " + " \"must\": { " + " \"script_score\": { " + " \"query\": { \"match_all\": {} }, " + " \"script\": { " + " \"source\": \"cosineSimilarity(params.queryVector, 'embedding') + 0.1\", " + " \"params\": { \"queryVector\": ?0 } " + " } " + " } " + " }, " + " \"filter\": [ " + " { \"term\": { \"color\": ?1 } }, " + " { \"term\": { \"size\": ?2 } } " + " ] " + " } " + "}") List<Post> findBySimilarWithFilter(Vector<Double> queryVector, String color, String size); }
若无法修改字段类型,可使用自动生成的
keyword子字段匹配:{ "term": { "color.keyword": ?1 } }
2. 权重提升(Boost)功能
方式一:全局评分系数提升
直接在script_score的脚本中加入权重系数,整体提升符合条件结果的评分:
@Query("{ " + " \"bool\": { " + " \"must\": { " + " \"script_score\": { " + " \"query\": { \"match_all\": {} }, " + " \"script\": { " + " \"source\": \"(cosineSimilarity(params.queryVector, 'embedding') + 0.1) * params.boostFactor\", " + " \"params\": { " + " \"queryVector\": ?0, " + " \"boostFactor\": ?1 " + " } " + " } " + " } " + " }, " + " \"filter\": [ { \"term\": { \"color\": ?2 } } ] " + " } " + "}") List<Post> findBySimilarWithBoost(Vector<Double> queryVector, double boostFactor, String color);
方式二:特定字段匹配提升
对标题等核心字段的匹配结果额外加权,结合bool的should节点实现:
@Query("{ " + " \"bool\": { " + " \"should\": [ " + " { " + " \"script_score\": { " + " \"query\": { \"match_all\": {} }, " + " \"script\": { " + " \"source\": \"cosineSimilarity(params.queryVector, 'embedding') + 0.1\", " + " \"params\": { \"queryVector\": ?0 } " + " } " + " } " + " }, " + " { " + " \"match\": { " + " \"title\": { " + " \"query\": ?1, " + " \"boost\": 2.0 " + " } " + " } " + " } " + " ], " + " \"filter\": [ { \"term\": { \"color\": ?2 } } ] " + " } " + "}") List<Post> findBySimilarWithFieldBoost(Vector<Double> queryVector, String titleKeyword, String color);
3. 分面(Facet)功能
分面查询依赖Elasticsearch的聚合能力,需通过ElasticsearchOperations构建查询并解析聚合结果:
服务层实现
@Service public class PostSearchService { private final ElasticsearchOperations elasticsearchOperations; public PostSearchService(ElasticsearchOperations elasticsearchOperations) { this.elasticsearchOperations = elasticsearchOperations; } public SearchHits<Post> searchWithFacets(Vector<Double> queryVector, String colorFilter) { // 构建基础语义查询+过滤条件 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); boolQuery.must(QueryBuilders.scriptScoreQuery( QueryBuilders.matchAllQuery(), new Script(ScriptType.INLINE, "painless", "cosineSimilarity(params.queryVector, 'embedding') + 0.1", Map.of("queryVector", queryVector)) )); if (colorFilter != null && !colorFilter.isEmpty()) { boolQuery.filter(QueryBuilders.termQuery("color", colorFilter)); } // 添加分面聚合:按color和size分组统计 NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(boolQuery) .addAggregation(AggregationBuilders.terms("color_facet").field("color")) .addAggregation(AggregationBuilders.terms("size_facet").field("size")) .build(); return elasticsearchOperations.search(searchQuery, Post.class); } // 解析聚合结果为分面统计数据 public Map<String, Map<String, Long>> getFacetResults(SearchHits<Post> searchHits) { Map<String, Map<String, Long>> facets = new HashMap<>(); // 解析color分面 Terms colorTerms = searchHits.getAggregations().get("color_facet"); if (colorTerms != null) { Map<String, Long> colorCounts = new HashMap<>(); for (Terms.Bucket bucket : colorTerms.getBuckets()) { colorCounts.put(bucket.getKeyAsString(), bucket.getDocCount()); } facets.put("color", colorCounts); } // 解析size分面 Terms sizeTerms = searchHits.getAggregations().get("size_facet"); if (sizeTerms != null) { Map<String, Long> sizeCounts = new HashMap<>(); for (Terms.Bucket bucket : sizeTerms.getBuckets()) { sizeCounts.put(bucket.getKeyAsString(), bucket.getDocCount()); } facets.put("size", sizeCounts); } return facets; } }
业务代码调用示例
// 执行带分面的语义搜索 SearchHits<Post> searchResults = postSearchService.searchWithFacets(queryEmbedding, "red"); // 获取搜索结果列表 List<Post> posts = searchResults.stream().map(SearchHit::getContent).collect(Collectors.toList()); // 获取分面统计数据(如各color的数量、各size的数量) Map<String, Map<String, Long>> facets = postSearchService.getFacetResults(searchResults);
内容的提问来源于stack exchange,提问作者Shahid Salim
相关产品推荐
相关产品推荐

