混合搜索与 Rank Fusion(语义 + 关键词)

概述

混合搜索在 Fess 中结合了传统的关键词搜索(BM25)与语义(向量)搜索,并通过 Rank Fusion 将两组结果合并,以生成更准确、更相关的排名。Rank Fusion 会将多个搜索器的结果整合为单一的优化排名。

在 Fess 15.9 中,语义搜索(内容分块 + 向量搜索)作为核心功能提供。启用后,语义搜索器会自动 注册到 Rank Fusion 中。有关如何配置,请参阅 语义搜索(内容分块 + 向量搜索)。

Fess 的 Rank Fusion 功能可以整合多个搜索结果, 提供更精确的搜索结果。

什么是 Rank Fusion

Rank Fusion 是一种将多个搜索算法或评分方法(例如关键词/BM25 与语义/向量搜索)的结果 组合起来,生成单一优化排名的技术。

主要优点:

  • 结合不同算法的优势

  • 提高搜索精度

  • 提供多样化的搜索结果

支持的算法

Fess 支持 RRF(Reciprocal Rank Fusion)算法进行 Rank Fusion。

RRF (Reciprocal Rank Fusion)

RRF 通过对每个搜索结果中文档排名的倒数求和来计算分数。 当一个文档被多个搜索器检索到时,其各项分数会被累加。

计算公式:

score(d) = Σ 1 / (k + rank(d))
  • k: 调整排名影响程度的常数参数(默认值:20)

  • rank(d): 文档 d 在各搜索结果中的排名(从 0 开始)

  • Σ: 对文档 d 出现的所有搜索器求和

Note

由 Fess 执行融合时(默认),融合算法固定为 RRF,也不支持按搜索器设置权重。各搜索器的贡献 会以相同的权重进行合计,唯一能够调整排名倾向的设置是 rank.fusion.rank_constant。 由搜索引擎执行融合时,可以使用其他组合方式,并可按搜索器指定权重(请参阅 在搜索引擎中执行 Rank Fusion)。

配置

fess_config.properties

基本配置:

# 窗口大小(融合目标的结果数量)
# 注意:必须 >= paging.search.page.max.size × 2。
# 如果设定值低于此最小值,将自动使用最小值。
rank.fusion.window_size=200

# RRF 的 rank_constant(k 参数)
rank.fusion.rank_constant=20

# 并行处理的线程数(0 或以下时,使用可用 CPU 核心数 × 3 ÷ 2 + 1)
rank.fusion.threads=-1

# 分数字段名(存储融合后分数的字段)
rank.fusion.score_field=rf_score
属性 默认值 说明
rank.fusion.window_size 200 从各搜索器中获取用于融合的最大结果数量。必须 >= paging.search.page.max.size × 2(默认为 200);若设定值低于此最小值,将自动提升至该最小值(启动时会输出 WARN 日志)。
rank.fusion.rank_constant 20 RRF 计算公式中的常数 k。值越大,高排名与低排名结果之间的分数差越小。
rank.fusion.threads -1 并行运行多个搜索器的固定线程池的线程数。指定 0 或以下时,将自动使用 可用 CPU 核心数 × 3 ÷ 2 + 1(由于是整数运算,小数部分会被舍去。例如:4 核 → 7,5 核 → 8)。
rank.fusion.score_field rf_score 用于存储融合后分数的结果文档字段名。

Note

配置的生效时机

以上 4 项配置的更改均需要重启 Fess 才能生效。从 fess_config.properties 读取的值会被缓存在 JVM 内,因此在运行过程中改写该文件不会生效。

另外,rank.fusion.window_size 仅在启动时读取一次,rank.fusion.threads 则在创建线程池时读取。线程池是在注册了 default 以外的搜索器 (例如语义搜索器)时创建的,因此在语义搜索未启用的情况下,线程池本身不会被创建。

JVM 系统属性

使用的搜索器通过 JVM 系统属性指定。 在 fess.in.sh 中添加如下内容:

FESS_JAVA_OPTS="$FESS_JAVA_OPTS -Drank.fusion.searchers=default,semantic_chunk"

如果是 fess.in.bat,则按如下方式添加:

set FESS_JAVA_OPTS=%FESS_JAVA_OPTS% -Drank.fusion.searchers=default,semantic_chunk

此属性的行为如下:

  • 以 JVM 选项形式设置,而非在 fess_config.properties 中配置。键名请直接指定 rank.fusion.searchers 本身。其他设置中常用的 -Dfess.config. 或 -Dfess.system. 前缀形式(例如 -Dfess.config.rank.fusion.searchers)不会被识别。

  • 除了 JVM 选项之外,也可以在管理界面“系统 > 常规”的“系统属性”栏中,以 rank.fusion.searchers=default,semantic_chunk 这样的形式写成一行。 但该栏中的值仅在同名系统属性尚未设置时才会被应用。 因此,若已通过 -D 指定,则 JVM 选项优先;而要更改已经应用的值, 需要重启 Fess。

  • default 是执行标准关键词搜索的搜索器,始终可用。

  • 搜索器的名称由其实现类名去掉末尾的 Searcher,再转换为小写蛇形命名(snake_case)得来 (SemanticChunkSearcher → semantic_chunk)。核心集成的语义搜索器 (语义搜索(内容分块 + 向量搜索))注册的名称为 semantic_chunk。

  • 若未指定此属性,将使用所有已注册的搜索器。若指定的名称与任何已注册搜索器均不匹配,则仅使用 default 搜索器。如果您使用核心集成的语义搜索器(语义搜索(内容分块 + 向量搜索)),通常完全不需要设置此属性。

  • 结果融合仅在可用搜索器为 2 个或以上时执行。若只有 1 个搜索器可用,则不进行融合,直接返回普通搜索结果。

Warning

如果您此前在 Fess 15.7 或更早版本中使用过 fess-webapp-semantic-search 插件,可能曾被 告知要将此属性设置为 -Drank.fusion.searchers=default,semantic。该插件将其搜索器注册为 名称 semantic,这与 15.9 中引入的核心集成搜索器名称 semantic_chunk 是不同的 搜索器。如果您原样将这个 15.7 时代的设置带入 15.9,允许列表中将永远不包含 semantic_chunk,导致核心集成的语义搜索(内容分块 + 向量搜索)完全无法工作——Fess 会静默地继续返回普通关键词搜索结果(启动时会记录一条警告日志,但每次请求的排除行为本身仅以 DEBUG 级别记录)。如果您的配置中指定了 default,semantic,请移除该设置,或为其添加 semantic_chunk。详情请参阅 语义搜索(内容分块 + 向量搜索) 中的“从 15.7 及更早版本迁移”一节。

在搜索引擎中执行 Rank Fusion

设置 rank.fusion.engine.enabled=true 后,融合将由 OpenSearch 而非 Fess 执行。 各搜索器的查询会合并为一个搜索请求(hybrid 查询),并由 OpenSearch 的搜索管道对分数进行 归一化和组合。由于融合在一次请求中完成,总命中数量和分面数量也反映融合后的结果集。

此功能需要 OpenSearch 的 neural-search 插件。标准发行版的 OpenSearch 和 ghcr.io/codelibs/fess-opensearch 镜像中已包含该插件,但 minimal 发行版中不包含。

配置写入 fess_config.properties(更改后需要重启 Fess 才能生效):

rank.fusion.engine.enabled=true
rank.fusion.combination.technique=rrf
rank.fusion.normalization.technique=min_max
rank.fusion.combination.weights=
rank.fusion.pagination_depth=1000
属性 默认值 说明
rank.fusion.engine.enabled false 设为 true 时由搜索引擎执行融合。
rank.fusion.combination.technique rrf 分数的组合方式:rrf、arithmetic_mean、geometric_mean 或 harmonic_mean。对于 rrf,rank.fusion.rank_constant 会用作常数 k,计算公式与由 Fess 执行融合时相同。
rank.fusion.normalization.technique min_max 组合前对分数进行归一化的方式:min_max、l2 或 z_score。rrf 不使用此设置。
rank.fusion.combination.weights (空) 各搜索器的权重,以逗号分隔的 名称:权重 对指定(例如 default:0.7,semantic_chunk:0.3)。每个权重必须在 0.0 到 1.0 之间,权重之和必须为 1.0,并且必须指定参与融合的所有搜索器。为空时各搜索器权重相等。不满足这些规则的值会输出 ERROR 日志,该次搜索将由 Fess 执行融合。
rank.fusion.pagination_depth 1000 每个搜索器在每个分片上提供给融合的结果数量。它限制了客户端可以翻页的深度,以及参与融合的文档范围。由搜索引擎融合的搜索最多可以翻页到这么多条结果。发送给搜索引擎的值不会超过 indexer.max.result.window.size。

Note

z_score 归一化只能与 arithmetic_mean 组合使用。这是 neural-search 插件的限制。 如果与 geometric_mean 或 harmonic_mean 组合,Fess 会拒绝该组合,输出指明这两个设置的 ERROR 日志,并由 Fess 对该次搜索执行融合。

在以下情况下,不会由搜索引擎执行融合,而是改由 Fess 融合结果。

  • 包含高级搜索参数(as.*)的搜索

  • 指定了排序、位置信息搜索或相似文档搜索的搜索。这些搜索会完全跳过语义搜索,因此结果仅来自 关键词搜索(请参阅 语义搜索(内容分块 + 向量搜索))。

  • rank.fusion.combination.weights 的值不满足上述规则时

  • z_score 归一化与 geometric_mean 或 harmonic_mean 组合时(会输出指明这两个设置的 ERROR 日志)

  • 搜索引擎完全无法执行融合请求时(无法识别 hybrid 查询或搜索管道的处理器,例如缺少 neural-search 插件或其版本过旧)。每次这样的搜索都会输出 WARN 日志。下一次搜索会再次请求 OpenSearch 执行融合,因此安装或升级插件后,无需重启 Fess 即可恢复由 OpenSearch 执行融合。

融合请求因其他原因失败时(无效的查询、已关闭的索引、分片故障等),与不由搜索引擎执行融合时一样, 仅针对该次搜索进行报告,下一次搜索会再次由搜索引擎执行融合。

由搜索引擎执行融合时,请注意以下几点。

  • rank.fusion.timeout 不适用。查询的嵌入会在发送搜索请求之前同步计算,因此如果嵌入提供商 响应缓慢或无响应,搜索会一直等待,最长直到提供商自身的超时时间(例如 content_chunker.embedding.ollama.timeout)。如果提供商会重试,重试的次数也会累加进来。 例如,内置的 opensearch 提供商会将 content_chunker.embedding.opensearch.timeout(默认 60000 毫秒)的等待最多重复 content_chunker.embedding.opensearch.retry.max(默认 3) 次,因此在提供商无响应时,默认情况下搜索可能被延迟 180 秒,再加上重试之间的等待时间。

  • rank.fusion.window_size 和 rank.fusion.threads 仅在由 Fess 执行融合时使用。

  • 由搜索引擎融合的搜索最多只能翻页到 rank.fusion.pagination_depth 条结果,因为搜索引擎 只融合每个搜索器在每个分片上排名前 rank.fusion.pagination_depth 的结果。总页数、是否有 下一页以及页码都止于此,请求起始位置(start)大于等于 rank.fusion.pagination_depth 的 页面时,会返回与其他搜索请求超过 index.max_result_window 的页面时相同的错误(在 v2 搜索 API 中为 HTTP 400 的 invalid_request)。即使在此范围内,从最后一条融合结果之后开始的 页面(例如过时的链接)也会返回空页面。不由搜索引擎融合的搜索仍与以前一样,可以翻页到 index.max_result_window。

  • 总命中数量低于 rank.fusion.pagination_depth 时是精确的。达到或超过该值时,搜索引擎统计的 命中数量可能少于实际匹配的数量,因此会作为下限报告(搜索 API 的 record_count_relation 为 GREATER_THAN_OR_EQUAL_TO)。

  • 增大 rank.fusion.pagination_depth 可以翻到更深的页面,但每次融合搜索都会按搜索器和分片收集 这么多条结果,因此不仅是深层页面,所有搜索都会变慢。

  • 语义搜索器 knn 查询的 k(每个分片的近邻数量)为 content_chunker.search.knn.k。 向量搜索即使在相似度较低时也会返回近邻,因此在未设置 content_chunker.search.min_score 的情况下,总命中数量会包含这些向量搜索的命中,在小规模索引中 可能涵盖用户可见的大部分文档。若要缩小范围,请设置 content_chunker.search.min_score(请参阅 语义搜索(内容分块 + 向量搜索))。

  • 搜索结果中照常附加 searcher 字段,但不会附加 rf_score。

与混合搜索的集成

Rank Fusion 在结合关键词搜索与语义搜索的 混合搜索中尤为有效。要使用语义搜索,请在配置内容分块功能之后设置 content_chunker.search.enabled=true。

Warning

content_chunker.enabled 和 content_chunker.search.enabled 等 content_chunker.* 配置属于系统属性,而不是 fess_config.properties。 请将其写入 conf/system.properties,或以 -Dfess.system.content_chunker.search.enabled=true 这样的形式指定为 JVM 选项。 即使写入 fess_config.properties 也不会生效。 另外,content_chunker.search.enabled 仅在启动时评估,因此启用后需要重启 Fess。

详情请参阅 语义搜索(内容分块 + 向量搜索)。

融合结果的确认

要确认 Rank Fusion 是否确实在工作,可以查看搜索结果中附加的以下两个字段。

字段 内容
searcher 检索到该文档的搜索器名称数组(例如 ["default", "semantic_chunk"])。若两者都包含在内,则表示该文档在关键词搜索和语义搜索中均被命中。
rf_score 通过 RRF 计算出的融合后分数。字段名可通过 rank.fusion.score_field 更改。

这两个字段都是在搜索时动态附加的值,不会保存到索引中。 此外,默认情况下它们不会包含在 /api/v2/search 的响应中,若要确认,请在 fess_config.properties 中进行以下设置并重启 Fess:

query.additional.api.response.fields=rf_score,searcher

Note

query.additional.api.response.fields 用于向“允许包含在 v2 搜索 API 响应中的字段” 许可列表中追加条目。如果追加 role 或 virtual_host 等访问控制用字段, 访问控制信息将暴露在搜索 API 的响应中,因此请勿追加这类字段。

对命中数量的影响

由 Fess 执行 Rank Fusion 时,返回的总命中数量并非直接使用主搜索器(注册在首位的 default 搜索器)的数量,而是按如下方式进行修正:

总命中数量 = 主搜索器的总命中数量 + 修正值

修正值是指:在融合后的前 window_size ÷ 2 条结果中,未包含在主搜索器前 window_size ÷ 2 条结果内的文档数量。也就是说,命中数量会增加仅由语义搜索找到的 文档数量。 因此,即使是相同的查询,启用与不启用混合搜索时的命中数量也可能不同。

另外,当主搜索器的总命中数量以概算值(下限值)返回时,不会进行此修正。 另外,分面(标签等)的数量会原样采用主搜索器的搜索结果。

由搜索引擎执行融合时(请参阅 在搜索引擎中执行 Rank Fusion),总命中数量和分面数量统计的是关键词搜索与 语义搜索两者命中结果的并集。每个搜索器在每个分片上最多提供 rank.fusion.pagination_depth 条 结果,语义搜索器还额外受 content_chunker.search.knn.k 的限制。因此,同一个查询在切换 rank.fusion.engine.enabled 后,命中数量和分面数量都会不同;在小规模索引中,由搜索引擎执行融合 时因为加入了语义搜索的命中,数量往往更大。

使用示例

基本混合搜索

  1. 通过关键词搜索计算 BM25 分数

  2. 通过语义搜索计算向量相似度

  3. 使用 RRF 融合两种结果

  4. 生成最终排名

搜索流程:

User Query
    ↓
┌──────────────────┬──────────────────┐
│  Keyword Search  │ Semantic Search  │
│    (BM25)        │  (Vector)        │
└────────┬─────────┴────────┬─────────┘
         ↓                  ↓
     Rank List 1        Rank List 2
         └────────┬─────────┘
                  ↓
          Rank Fusion (RRF)
                  ↓
          Final Ranking

性能注意事项

内存使用

  • 由于需要保留多个搜索结果,内存使用量会增加。

  • 可使用 rank.fusion.window_size 限制融合目标的最大数量。主搜索器(排在首位的 default 搜索器)最多获取 window_size 条结果,其他搜索器各获取 window_size ÷ 搜索器数量 条结果(搜索器数量 是包含主搜索器在内的总数,除法会向下取整)。

  • 例如,当有 2 个搜索器(default 和 semantic_chunk)且 window_size=200 时,主搜索器获取 200 条、语义搜索器获取 100 条,因此最多会保留 300 条文档。

# 融合目标的窗口大小
rank.fusion.window_size=200

Warning

rank.fusion.window_size 不能低于 paging.search.page.max.size × 2。 当 paging.search.page.max.size 为默认值 100 时,下限即为 200,这与 rank.fusion.window_size 的默认值相同。也就是说,在默认配置下无法将 window_size 设置为小于默认值。即使设置了更小的值,启动时也会输出 WARN 日志并将其提升至 200。 若要实际减小该值,需要先降低 paging.search.page.max.size,但这同时也会降低搜索界面 和 API 中每页可请求的最大结果数量。

处理时间

  • 由于需要执行多次搜索,响应时间会增加。

  • 使用 rank.fusion.threads 设置并行执行的线程数。

# 并行执行的线程数(0 或以下时,使用可用 CPU 核心数 × 3 ÷ 2 + 1)
rank.fusion.threads=-1

Note

由 Fess 执行融合时,主搜索器以外的搜索器最多等待 rank.fusion.timeout 毫秒(默认 10000)。届时仍未响应的搜索器会从该次搜索中排除,搜索结果会被标记为部分结果(超时)。 主搜索器始终会等待至完成。指定 0 或以下时将无限制地等待。由搜索引擎执行融合时,此设置 不适用(请参阅 在搜索引擎中执行 Rank Fusion)。

搜索器故障时的行为

当任一搜索器因异常而失败时,该搜索器的结果会被视为空,在输出 WARN 日志之后, 仅使用其余搜索器的结果继续进行融合。搜索请求本身不会变成错误。

但查询语法错误(InvalidQueryException)和分页上限超出 (ResultOffsetExceededException)除外,它们会直接作为错误返回。 此外,在不进行融合的深层页面(起始位置 × 2 大于等于 rank.fusion.window_size 的位置), 主搜索器中发生的异常会直接作为搜索请求的错误返回。

语义搜索器在无法连接到嵌入提供商或嵌入处理失败时,会返回空结果。 这种情况下同样不会产生错误,而是仅返回关键词搜索的结果。

故障排除

搜索结果与预期不符

症状:Rank Fusion 后的结果与预期不符

检查事项:

  1. 确认 searcher 字段(请参阅“融合结果的确认”)。如果所有文档都仅为 ["default"],则说明语义搜索器没有返回结果。

  2. 确认语义搜索是否被跳过。除了包含短语、通配符等搜索语法的查询之外,在排序搜索、位置信息搜索、 相似文档搜索时,语义搜索器也不会返回结果,仅返回关键词搜索的结果。 跳过条件的详细信息请参阅 语义搜索(内容分块 + 向量搜索)。

  3. 分别确认各搜索类型的结果

  4. 调整 rank.fusion.rank_constant 的值

  5. 在翻页较深的页面(起始位置 × 2 大于等于 rank.fusion.window_size 的位置,默认情况下 为第 101 条之后)不会执行融合,仅使用主搜索器进行搜索。若希望在更多页面上获得融合结果, 请增大 rank.fusion.window_size。由搜索引擎执行融合时没有此分界, 最多 rank.fusion.pagination_depth 条结果以内的所有页面都会被融合(请参阅 在搜索引擎中执行 Rank Fusion)。

搜索缓慢

症状:启用 Rank Fusion 时搜索变慢

解决方法:

  1. 调整 rank.fusion.threads:

    rank.fusion.threads=4
    
  2. 减小 rank.fusion.window_size。但由于不能低于下限 (paging.search.page.max.size × 2),在默认配置下需要将以下两项成对设置:

    paging.search.page.max.size=50
    rank.fusion.window_size=100
    

    请注意,每页可请求的最大结果数量也会随之下降。设置后需要重启。

  3. 由搜索引擎执行融合时,减小 rank.fusion.pagination_depth。请注意,融合结果可以翻页的 深度也会随之变浅。设置后需要重启。

内存不足

症状:发生 OutOfMemoryError

解决方法:

  1. 按照“搜索缓慢”中的相同步骤减小 rank.fusion.window_size

  2. 增加 JVM 堆大小

参考信息