倒数排名融合编辑

此功能处于技术预览阶段,可能会在未来版本中更改或删除。语法在正式发布之前可能会发生变化。Elastic 将努力解决任何问题,但技术预览版中的功能不受官方正式发布版功能的支持 SLA 的约束。

倒数排名融合 (RRF) 是一种将具有不同相关性指标的多个结果集组合成单个结果集的方法。RRF 不需要调整,并且不同的相关性指标不需要彼此相关即可获得高质量的结果。

RRF 使用以下公式来确定每个文档的排名分数

score = 0.0
for q in queries:
    if d in result(q):
        score += 1.0 / ( k + rank( result(q), d ) )
return score

# where
# k is a ranking constant
# q is a query in the set of queries
# d is a document in the result set of q
# result(q) is the result set of q
# rank( result(q), d ) is d's rank within the result(q) starting from 1

倒数排名融合 API编辑

您可以将 RRF 用作 搜索 的一部分,以使用来自 子检索器 组合的单独顶级文档集(结果集)使用 RRF 检索器 来组合和对文档进行排名。排名至少需要 两个 子检索器。

RRF 检索器是一个可选对象,定义为搜索请求的 检索器参数 的一部分。RRF 检索器对象包含以下参数

检索器

(必需,检索器对象数组)

子检索器列表,用于指定将对哪些返回的顶级文档集应用 RRF 公式。每个子检索器在 RRF 公式中具有相同的权重。需要两个或多个子检索器。

rank_constant

(可选,整数)

此值确定每个查询的各个结果集中的文档对最终排名结果集的影响程度。值越高表示排名较低的文档影响越大。此值必须大于或等于 1。默认为 60

window_size

(可选,整数)

此值确定每个查询的各个结果集的大小。值越高,结果相关性越好,但会以性能为代价。最终排名结果集将被修剪到搜索请求的 大小window_size 必须大于或等于 size 并且大于或等于 1。默认为 size 参数。

使用 RRF 的请求示例

GET example-index/_search
{
    "retriever": {
        "rrf": { 
            "retrievers": [
                {
                    "standard": { 
                        "query": {
                            "term": {
                                "text": "shoes"
                            }
                        }
                    }
                },
                {
                    "knn": { 
                        "field": "vector",
                        "query_vector": [1.25, 2, 3.5],
                        "k": 50,
                        "num_candidates": 100
                    }
                }
            ],
            "window_size": 50,
            "rank_constant": 20
        }
    }
}

在上面的示例中,我们分别执行 knnstandard 检索器。然后我们使用 rrf 检索器组合结果。

首先,我们执行由 knn 检索器指定的 kNN 搜索以获得其全局前 50 个结果。

其次,我们执行由 standard 检索器指定的查询以获得其全局前 50 个结果。

然后,在协调节点上,我们将 kNN 搜索顶级文档与查询顶级文档组合在一起,并使用 rrf 检索器中的参数根据 RRF 公式对它们进行排名,以使用默认 size 10 获取组合的顶级文档。

请注意,如果 knn 搜索中的 k 大于 window_size,则结果将被截断为 window_size。如果 k 小于 window_size,则结果为 k 大小。

倒数排名融合支持的功能编辑

rrf 检索器支持

rrf 检索器当前不支持

在使用 rrf 检索器的搜索中使用不受支持的功能会导致异常。

使用多个标准检索器的倒数排名融合编辑

rrf 检索器提供了一种组合和对多个 standard 检索器进行排名的方法。主要用例是组合来自传统 BM25 查询和 ELSER 查询的顶级文档,以提高相关性。

使用 RRF 和多个标准检索器的请求示例

GET example-index/_search
{
    "retriever": {
        "rrf": { 
            "retrievers": [
                {
                    "standard": { 
                        "query": {
                            "term": {
                                "text": "blue shoes sale"
                            }
                        }
                    }
                },
                {
                    "standard": { 
                        "query": {
                            "text_expansion":{
                                "ml.tokens":{
                                    "model_id":"my_elser_model",
                                    "model_text":"What blue shoes are on sale?"
                                }
                            }
                        }
                    }
                }
            ],
            "window_size": 50,
            "rank_constant": 20
        }
    }
}

在上面的示例中,我们分别执行两个 standard 检索器。然后我们使用 rrf 检索器组合结果。

首先,我们运行 standard 检索器,使用标准 BM25 评分算法为 blue shoes sales 指定一个词条查询。

接下来,我们运行 standard 检索器,使用我们的 ELSER 评分算法为 What blue shoes are on sale? 指定一个文本扩展查询。

rrf 检索器允许我们组合由完全独立的评分算法生成的两个顶级文档集,并赋予它们相同的权重。

这不仅消除了使用线性组合找出适当权重的需要,而且 RRF 还被证明比单独使用任何一个查询都能提供更高的相关性。

使用子搜索的倒数排名融合编辑

不再支持使用子搜索的 RRF。请改用 检索器 API。有关示例,请参阅使用多个标准检索器

倒数排名融合完整示例编辑

我们首先为索引创建一个映射,该映射包含一个文本字段、一个向量字段和一个整数字段,以及索引多个文档。在本例中,我们将使用只有一个维度的向量,以便更容易解释排名。

PUT example-index
{
    "mappings": {
        "properties": {
            "text" : {
                "type" : "text"
            },
            "vector": {
                "type": "dense_vector",
                "dims": 1,
                "index": true,
                "similarity": "l2_norm"
            },
            "integer" : {
                "type" : "integer"
            }
        }
    }
}

PUT example-index/_doc/1
{
    "text" : "rrf",
    "vector" : [5],
    "integer": 1
}

PUT example-index/_doc/2
{
    "text" : "rrf rrf",
    "vector" : [4],
    "integer": 2
}

PUT example-index/_doc/3
{
    "text" : "rrf rrf rrf",
    "vector" : [3],
    "integer": 1
}

PUT example-index/_doc/4
{
    "text" : "rrf rrf rrf rrf",
    "integer": 2
}

PUT example-index/_doc/5
{
    "vector" : [0],
    "integer": 1
}

POST example-index/_refresh

现在,我们使用 rrf 检索器执行搜索,该检索器包含一个指定 BM25 查询的 standard 检索器、一个指定 kNN 搜索的 knn 检索器和一个词条聚合。

GET example-index/_search
{
    "retriever": {
        "rrf": {
            "retrievers": [
                {
                    "standard": {
                        "query": {
                            "term": {
                                "text": "rrf"
                            }
                        }
                    }
                },
                {
                    "knn": {
                        "field": "vector",
                        "query_vector": [3],
                        "k": 5,
                        "num_candidates": 5
                    }
                }
            ],
            "window_size": 5,
            "rank_constant": 1
        }
    },
    "size": 3,
    "aggs": {
        "int_count": {
            "terms": {
                "field": "integer"
            }
        }
    }
}

我们收到带有排名 hits 和词条聚合结果的响应。请注意,_scorenull,我们改为使用 _rank 来显示排名靠前的文档。

{
    "took": ...,
    "timed_out" : false,
    "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
    },
    "hits" : {
        "total" : {
            "value" : 5,
            "relation" : "eq"
        },
        "max_score" : null,
        "hits" : [
            {
                "_index" : "example-index",
                "_id" : "3",
                "_score" : null,
                "_rank" : 1,
                "_source" : {
                    "integer" : 1,
                    "vector" : [
                        3
                    ],
                    "text" : "rrf rrf rrf"
                }
            },
            {
                "_index" : "example-index",
                "_id" : "2",
                "_score" : null,
                "_rank" : 2,
                "_source" : {
                    "integer" : 2,
                    "vector" : [
                        4
                    ],
                    "text" : "rrf rrf"
                }
            },
            {
                "_index" : "example-index",
                "_id" : "4",
                "_score" : null,
                "_rank" : 3,
                "_source" : {
                    "integer" : 2,
                    "text" : "rrf rrf rrf rrf"
                }
            }
        ]
    },
    "aggregations" : {
        "int_count" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
                {
                    "key" : 1,
                    "doc_count" : 3
                },
                {
                    "key" : 2,
                    "doc_count" : 2
                }
            ]
        }
    }
}

让我们分解一下这些匹配项是如何排名的。我们首先分别运行指定查询的 standard 检索器和指定 kNN 搜索的 knn 检索器,以收集它们各自的匹配项。

首先,我们查看来自 standard 检索器的查询匹配项。

"hits" : [
    {
        "_index" : "example-index",
        "_id" : "4",
        "_score" : 0.16152832,              
        "_source" : {
            "integer" : 2,
            "text" : "rrf rrf rrf rrf"
        }
    },
    {
        "_index" : "example-index",
        "_id" : "3",                        
        "_score" : 0.15876243,
        "_source" : {
            "integer" : 1,
            "vector" : [3],
            "text" : "rrf rrf rrf"
        }
    },
    {
        "_index" : "example-index",
        "_id" : "2",                        
        "_score" : 0.15350538,
        "_source" : {
            "integer" : 2,
            "vector" : [4],
            "text" : "rrf rrf"
        }
    },
    {
        "_index" : "example-index",
        "_id" : "1",                        
        "_score" : 0.13963442,
        "_source" : {
            "integer" : 1,
            "vector" : [5],
            "text" : "rrf"
        }
    }
]

排名 1,_id 4

排名 2,_id 3

排名 3,_id 2

排名 4,_id 1

请注意,我们的第一个匹配项没有 vector 字段的值。现在,我们查看来自 knn 检索器的 kNN 搜索结果。

"hits" : [
    {
        "_index" : "example-index",
        "_id" : "3",                   
        "_score" : 1.0,
        "_source" : {
            "integer" : 1,
            "vector" : [3],
            "text" : "rrf rrf rrf"
        }
    },
    {
        "_index" : "example-index",
        "_id" : "2",                   
        "_score" : 0.5,
        "_source" : {
            "integer" : 2,
            "vector" : [4],
            "text" : "rrf rrf"
        }
    },
    {
        "_index" : "example-index",
        "_id" : "1",                   
        "_score" : 0.2,
        "_source" : {
            "integer" : 1,
            "vector" : [5],
            "text" : "rrf"
        }
    },
    {
        "_index" : "example-index",
        "_id" : "5",                   
        "_score" : 0.1,
        "_source" : {
            "integer" : 1,
            "vector" : [0]
        }
    }
]

排名 1,_id 3

排名 2,_id 2

排名 3,_id 1

排名 4,_id 5

现在,我们可以使用来自 rrf 检索器的参数将两个单独排名的结果集应用于 RRF 公式,以获得最终排名。

# doc  | query     | knn       | score
_id: 1 = 1.0/(1+4) + 1.0/(1+3) = 0.4500
_id: 2 = 1.0/(1+3) + 1.0/(1+2) = 0.5833
_id: 3 = 1.0/(1+2) + 1.0/(1+1) = 0.8333
_id: 4 = 1.0/(1+1)             = 0.5000
_id: 5 =             1.0/(1+4) = 0.2000

我们根据 RRF 公式对文档进行排名,window_size5,将 RRF 结果集中排名靠后的 2 个文档截断,size3。我们最终得到 _id: 3_rank: 1_id: 2_rank: 2_id: 4_rank: 3。此排名与原始 RRF 搜索的结果集相匹配,符合预期。

RRF 中的分页编辑

使用 rrf 时,您可以使用 from 参数对结果进行分页。由于最终排名完全取决于原始查询排名,为了确保分页时的一致性,我们必须确保在 from 更改时,我们已经看到的内容的顺序保持不变。为此,我们使用固定的 window_size 作为可以分页的整个可用结果集。这实质上意味着,如果

  • from + sizewindow_size:我们可以从最终的 rrf 排名结果集中获取 results[from: from+size] 个文档
  • from + size > window_size:我们将获得 0 个结果,因为请求将超出可用的 window_size 大小的结果集。

这里需要注意的一件重要事情是,由于 window_size 是我们将从各个查询组件中看到的所有结果,因此只有当 window_size 保持不变时,分页才能保证一致性,即在多个页面中没有文档被跳过或重复。如果 window_size 发生变化,则结果的顺序也可能会发生变化,即使对于相同的排名也是如此。

为了说明上述所有内容,让我们考虑以下简化示例,其中我们有两个查询,queryAqueryB 及其排名文档

     |  queryA   |  queryB    |
_id: |  1        |  5         |
_id: |  2        |  4         |
_id: |  3        |  3         |
_id: |  4        |  1         |
_id: |           |  2         |

对于 window_size=5,我们将看到来自 queryAqueryB 的所有文档。假设 rank_constant=1,则 rrf 分数将为

# doc   | queryA     | queryB       | score
_id: 1 =  1.0/(1+1)  + 1.0/(1+4)      = 0.7
_id: 2 =  1.0/(1+2)  + 1.0/(1+5)      = 0.5
_id: 3 =  1.0/(1+3)  + 1.0/(1+3)      = 0.5
_id: 4 =  1.0/(1+4)  + 1.0/(1+2)      = 0.533
_id: 5 =    0        + 1.0/(1+1)      = 0.5

所以最终的排名结果集将是 [1, 4, 2, 3, 5],我们将对此进行分页,因为 window_size == len(results)。在这种情况下,我们将有

  • from=0, size=2 将返回文档 [1, 4],排名为 [1, 2]
  • from=2, size=2 将返回文档 [2, 3],排名为 [3, 4]
  • from=4, size=2 将返回文档 [5],排名为 [5]
  • from=6, size=2 将返回一个空结果集,因为它没有更多结果可供迭代

现在,如果我们有一个 window_size=2,我们只能看到查询 queryAqueryB 分别对应的文档 [1, 2] 和 [5, 4]。通过计算,我们会发现结果现在略有不同,因为我们不知道任一查询中位置 [3: end] 的文档。

# doc   | queryA     | queryB         | score
_id: 1 =  1.0/(1+1)  + 0              = 0.5
_id: 2 =  1.0/(1+2)  + 0              = 0.33
_id: 4 =    0        + 1.0/(1+2)      = 0.33
_id: 5 =    0        + 1.0/(1+1)      = 0.5

最终的排名结果集将是 [1, 5, 2, 4],我们将能够对前 window_size 个结果进行分页,即 [1, 5]。因此,对于与上述相同的参数,我们现在将拥有

  • from=0, size=2 将返回 [1, 5],排名为 [1, 2]
  • from=2, size=2 将返回一个空结果集,因为它将超出可用的 window_size 结果。