如何在Elasticsearch中实现多来源搜索结果的多样化排序?
我从多个来源收集了文档,部分查询会返回大量同一来源的结果,这一情况并不理想。我不想过滤掉这些结果,而是希望对结果进行排序,让前几页的结果包含来自不同来源的内容。
文档结构示例:
{ "doc": 1, "name": "Programming languages. A deep dive.", "source": "blog1", "url": "https://example.com/" }
{ "doc": 2, "name": "Learn coding in 30 seconds. For real!", "source": "blog2", "url": "..." }
我的文档来自数千个网站,但部分网站(如blog1)拥有大量教程,其他网站则仅有少量。当我搜索热门词汇(如"chat bots")并获取前20条结果时,可能有5-10条来自blog1;进入第二页后,仍会看到大量blog1的结果,这看起来像是在推广该网站,并非我想要的效果。我希望前几页的结果更具多样性,但如果用户加载足够多的页面,仍能看到所有结果,因此过滤并非可行方案。
我考虑过统计每个来源已返回的文档数量,并以此降低该来源的权重,想知道这种方案能否在Elasticsearch query中实现?是否有其他替代方案?
补充说明:我理解通常这种需求难以实现,因为若提升某文档的分数至已返回文档之上会破坏排序规则,但我实际想要降低分数,这或许可行。Elasticsearch可在某条数据分数高于前一条时返回错误。
方案1:动态降权已出现的来源(函数评分+脚本字段)
你提到的统计已返回来源数量并降权的思路完全可行,核心是利用Elasticsearch的**函数评分查询(Function Score Query)**结合脚本,根据当前请求中已出现的来源列表动态调整文档评分。
实现逻辑:
- 在查询时,将当前页之前已经返回过的
source列表作为参数传入 - 通过脚本计算每个文档的来源在已返回列表中的出现次数,以此降低该文档的评分
示例查询:
{ "query": { "function_score": { "query": { "match": { "name": "chat bots" } }, "functions": [ { "script_score": { "script": { "source": """ def seen_sources = params.seen_sources; def source_count = seen_sources.stream().filter(s -> s == doc['source'].value).count(); // 每出现一次,分数乘以0.5,可根据需求调整系数 return Math.pow(0.5, source_count); """, "params": { "seen_sources": ["blog1", "blog3"] // 传入之前页面已返回的来源 } } } } ], "boost_mode": "multiply" // 降权系数与原始评分相乘 } }, "size": 20 }
注意:这种方式需要前端或服务端记录每一页返回的source列表,下一页查询时传入。好处是完全保留所有结果,只是动态调整排序,用户翻到后面仍能看到同一来源的大量内容。
方案2:使用collapse字段折叠(分页需特殊处理)
Elasticsearch的collapse功能可以按字段(这里是source)折叠结果,确保每个来源在当前页只出现一次,同时通过inner_hits获取该来源的其他文档。
示例查询:
{ "query": { "match": { "name": "chat bots" } }, "collapse": { "field": "source", "inner_hits": { "name": "other_docs_from_source", "size": 5 // 每个来源最多保留5条备用,可调整 } }, "size": 20 // 当前页返回20个不同来源的顶级文档 }
分页注意:默认的from/size分页对折叠后的结果不友好,需要使用search_after进行分页,避免重复或遗漏。这种方式前几页的来源多样性极强,但需要处理inner_hits的展示逻辑,用户可以通过展开查看同一来源的更多内容。
方案3:自定义分桶排序(结合Terms Aggregation)
先通过聚合获取所有匹配的来源,再按来源的文档数量倒序(或自定义规则),然后逐个来源取文档,组成分页结果。
实现步骤:
- 执行聚合查询,获取匹配
chat bots的所有source及对应的文档数 - 按来源文档数升序排序(优先取文档少的来源,保证多样性)
- 逐个来源执行查询,取指定数量的文档,直到凑够当前页的
size
这种方式需要服务端做二次处理,灵活性最高,可以完全控制前几页的来源分布,但性能会比直接查询稍差,适合数据量不是特别大的场景。
内容的提问来源于stack exchange,提问作者ivant

