相互ランク融合
この機能は技術プレビュー中であり、将来のリリースで変更または削除される可能性があります。GAの前に構文が変更される可能性があります。Elasticは問題を修正するために取り組みますが、技術プレビューの機能は公式GA機能のサポートSLAの対象ではありません。
相互ランク融合 (RRF)は、異なる関連性指標を持つ複数の結果セットを単一の結果セットに結合する方法です。RRFは調整を必要とせず、異なる関連性指標は高品質の結果を得るために互いに関連している必要はありません。
RRFは、各ドキュメントのランク付けスコアを決定するために次の式を使用します:
Python
score = 0.0
for q in queries:
if d in result(q):
score += 1.0 / ( k + rank( result(q), d ) )
return score
# どこ
# kはランク付け定数です
# qはクエリのセット内のクエリです
# dはqの結果セット内のドキュメントです
# result(q)はqの結果セットです
# rank( result(q), d )は、1から始まるresult(q)内のdのランクです
相互ランク融合API
RRFを検索の一部として使用して、子リトリーバーの組み合わせからの別々のトップドキュメントセット(結果セット)を使用してドキュメントを結合およびランク付けできます。RRFリトリーバーを使用するには、少なくとも2つの子リトリーバーが必要です。
RRFリトリーバーは、検索リクエストのリトリーバーパラメータの一部として定義されたオプションのオブジェクトです。RRFリトリーバーオブジェクトには次のパラメータが含まれます:
retrievers
- (必須、リトリーバーオブジェクトの配列)
RRF式が適用される返されたトップドキュメントのセットを指定するための子リトリーバーのリスト。各子リトリーバーはRRF式の一部として等しい重みを持ちます。2つ以上の子リトリーバーが必要です。 rank_constant
- (オプション、整数)
この値は、各クエリの個別の結果セット内のドキュメントが最終的なランク付け結果セットに与える影響の程度を決定します。値が高いほど、低ランクのドキュメントがより多くの影響を与えます。この値は1
以上でなければなりません。デフォルトは60
です。 rank_window_size
- (オプション、整数)
この値は、各クエリの個別の結果セットのサイズを決定します。値が高いほど、パフォーマンスのコストで結果の関連性が向上します。最終的なランク付け結果セットは、検索リクエストのサイズに基づいてプルーニングされます。rank_window_size
はsize
以上であり、1
以上でなければなりません。デフォルトはsize
パラメータです。
RRFを使用したリクエストの例:
Python
resp = client.search(
index="example-index",
retriever={
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"term": {
"text": "shoes"
}
}
}
},
{
"knn": {
"field": "vector",
"query_vector": [
1.25,
2,
3.5
],
"k": 50,
"num_candidates": 100
}
}
],
"rank_window_size": 50,
"rank_constant": 20
}
},
)
print(resp)
Js
const response = await client.search({
index: "example-index",
retriever: {
rrf: {
retrievers: [
{
standard: {
query: {
term: {
text: "shoes",
},
},
},
},
{
knn: {
field: "vector",
query_vector: [1.25, 2, 3.5],
k: 50,
num_candidates: 100,
},
},
],
rank_window_size: 50,
rank_constant: 20,
},
},
});
console.log(response);
コンソール
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
}
}
],
"rank_window_size": 50,
"rank_constant": 20
}
}
}
上記の例では、knn
およびstandard
リトリーバーを互いに独立して実行します。次に、rrf
リトリーバーを使用して結果を結合します。
最初に、knn リトリーバーによって指定されたkNN検索を実行して、そのグローバルトップ50結果を取得します。 |
|
次に、standard リトリーバーによって指定されたクエリを実行して、そのグローバルトップ50結果を取得します。 |
|
次に、コーディネートノードで、kNN検索のトップドキュメントとクエリのトップドキュメントを結合し、rrf リトリーバーのパラメータを使用してRRF式に基づいてランク付けし、デフォルトのsize を使用して10 のトップドキュメントを取得します。 |
## 相互ランク融合のサポートされている機能
`````rrf`````リトリーバーは次のことをサポートしています:
- [集約](/read/elasticsearch-8-15/396e71470b74cb38.md)
- [from](89159571f146b334.md#search-from-param)
`````rrf`````リトリーバーは現在次のことをサポートしていません:
- [スクロール](89159571f146b334.md#search-api-scroll-query-param)
- [ポイントインタイム](89159571f146b334.md#search-api-pit)
- [ソート](89159571f146b334.md#search-sort-param)
- [再スコア](3f786c09568adf05.md#rescore)
- [サジェスター](/read/elasticsearch-8-15/39d4e7248968ca4f.md)
- [ハイライト](/read/elasticsearch-8-15/44d174e4822907ce.md)
- [コラプス](/read/elasticsearch-8-15/c30d224de91b0a6e.md)
- [プロファイリング](237e89fe5c1c5ae9.md#profiling-queries)
`````rrf`````リトリーバーを使用した検索の一部としてサポートされていない機能を使用すると、例外が発生します。
## 複数の標準リトリーバーを使用した相互ランク融合
`````rrf`````リトリーバーは、複数の`````standard`````リトリーバーを結合およびランク付けする方法を提供します。主な使用ケースは、従来のBM25クエリと[ELSER](/read/elasticsearch-8-15/070e47d72203e3e4.md)クエリからのトップドキュメントを結合して関連性を向上させることです。
複数の標準リトリーバーを使用したRRFのリクエストの例:
#### Python
``````python
resp = client.search(
index="example-index",
retriever={
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"term": {
"text": "blue shoes sale"
}
}
}
},
{
"standard": {
"query": {
"sparse_vector": {
"field": "ml.tokens",
"inference_id": "my_elser_model",
"query": "What blue shoes are on sale?"
}
}
}
}
],
"rank_window_size": 50,
"rank_constant": 20
}
},
)
print(resp)
`
Js
const response = await client.search({
index: "example-index",
retriever: {
rrf: {
retrievers: [
{
standard: {
query: {
term: {
text: "blue shoes sale",
},
},
},
},
{
standard: {
query: {
sparse_vector: {
field: "ml.tokens",
inference_id: "my_elser_model",
query: "What blue shoes are on sale?",
},
},
},
},
],
rank_window_size: 50,
rank_constant: 20,
},
},
});
console.log(response);
コンソール
GET example-index/_search
{
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"term": {
"text": "blue shoes sale"
}
}
}
},
{
"standard": {
"query": {
"sparse_vector":{
"field": "ml.tokens",
"inference_id": "my_elser_model",
"query": "What blue shoes are on sale?"
}
}
}
}
],
"rank_window_size": 50,
"rank_constant": 20
}
}
}
上記の例では、2つのstandard
リトリーバーを互いに独立して実行します。次に、rrf
リトリーバーを使用して結果を結合します。
最初に、standard リトリーバーを実行し、標準BM25スコアリングアルゴリズムを使用してblue shoes sales の用語クエリを指定します。 |
|
次に、standard リトリーバーを実行し、ELSERスコアリングアルゴリズムを使用してWhat blue shoes are on sale? のスパースベクタークエリを指定します。 |
|
rrf リトリーバーは、完全に独立したスコアリングアルゴリズムによって生成された2つのトップドキュメントセットを等しい重みで結合することを可能にします。 |
これにより、線形結合を使用して適切な重みを決定する必要がなくなるだけでなく、RRFは個々のクエリよりも改善された関連性を提供することが示されています。
サブ検索を使用した相互ランク融合
サブ検索を使用したRRFはもはやサポートされていません。リトリーバーAPIを代わりに使用してください。複数の標準リトリーバーを使用するの例を参照してください。
相互ランク融合の完全な例
テキストフィールド、ベクターフィールド、整数フィールドを持つインデックスのマッピングを作成し、いくつかのドキュメントをインデックスします。この例では、ランク付けを説明しやすくするために、単一の次元のみを持つベクターを使用します。
Python
resp = client.indices.create(
index="example-index",
mappings={
"properties": {
"text": {
"type": "text"
},
"vector": {
"type": "dense_vector",
"dims": 1,
"index": True,
"similarity": "l2_norm",
"index_options": {
"type": "hnsw"
}
},
"integer": {
"type": "integer"
}
}
},
)
print(resp)
resp1 = client.index(
index="example-index",
id="1",
document={
"text": "rrf",
"vector": [
5
],
"integer": 1
},
)
print(resp1)
resp2 = client.index(
index="example-index",
id="2",
document={
"text": "rrf rrf",
"vector": [
4
],
"integer": 2
},
)
print(resp2)
resp3 = client.index(
index="example-index",
id="3",
document={
"text": "rrf rrf rrf",
"vector": [
3
],
"integer": 1
},
)
print(resp3)
resp4 = client.index(
index="example-index",
id="4",
document={
"text": "rrf rrf rrf rrf",
"integer": 2
},
)
print(resp4)
resp5 = client.index(
index="example-index",
id="5",
document={
"vector": [
0
],
"integer": 1
},
)
print(resp5)
resp6 = client.indices.refresh(
index="example-index",
)
print(resp6)
Js
const response = await client.indices.create({
index: "example-index",
mappings: {
properties: {
text: {
type: "text",
},
vector: {
type: "dense_vector",
dims: 1,
index: true,
similarity: "l2_norm",
index_options: {
type: "hnsw",
},
},
integer: {
type: "integer",
},
},
},
});
console.log(response);
const response1 = await client.index({
index: "example-index",
id: 1,
document: {
text: "rrf",
vector: [5],
integer: 1,
},
});
console.log(response1);
const response2 = await client.index({
index: "example-index",
id: 2,
document: {
text: "rrf rrf",
vector: [4],
integer: 2,
},
});
console.log(response2);
const response3 = await client.index({
index: "example-index",
id: 3,
document: {
text: "rrf rrf rrf",
vector: [3],
integer: 1,
},
});
console.log(response3);
const response4 = await client.index({
index: "example-index",
id: 4,
document: {
text: "rrf rrf rrf rrf",
integer: 2,
},
});
console.log(response4);
const response5 = await client.index({
index: "example-index",
id: 5,
document: {
vector: [0],
integer: 1,
},
});
console.log(response5);
const response6 = await client.indices.refresh({
index: "example-index",
});
console.log(response6);
コンソール
PUT example-index
{
"mappings": {
"properties": {
"text" : {
"type" : "text"
},
"vector": {
"type": "dense_vector",
"dims": 1,
"index": true,
"similarity": "l2_norm",
"index_options": {
"type": "hnsw"
}
},
"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
#### Python
``````python
resp = client.search(
index="example-index",
retriever={
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"term": {
"text": "rrf"
}
}
}
},
{
"knn": {
"field": "vector",
"query_vector": [
3
],
"k": 5,
"num_candidates": 5
}
}
],
"rank_window_size": 5,
"rank_constant": 1
}
},
size=3,
aggs={
"int_count": {
"terms": {
"field": "integer"
}
}
},
)
print(resp)
`
Js
const response = await client.search({
index: "example-index",
retriever: {
rrf: {
retrievers: [
{
standard: {
query: {
term: {
text: "rrf",
},
},
},
},
{
knn: {
field: "vector",
query_vector: [3],
k: 5,
num_candidates: 5,
},
},
],
rank_window_size: 5,
rank_constant: 1,
},
},
size: 3,
aggs: {
int_count: {
terms: {
field: "integer",
},
},
},
});
console.log(response);
コンソール
GET example-index/_search
{
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"term": {
"text": "rrf"
}
}
}
},
{
"knn": {
"field": "vector",
"query_vector": [3],
"k": 5,
"num_candidates": 5
}
}
],
"rank_window_size": 5,
"rank_constant": 1
}
},
"size": 3,
"aggs": {
"int_count": {
"terms": {
"field": "integer"
}
}
}
}
ランク付けされたhits
と用語集約結果を持つ応答を受け取ります。ランカーのscore
と_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" : 0.8333334,
"_rank" : 1,
"_source" : {
"integer" : 1,
"vector" : [
3
],
"text" : "rrf rrf rrf"
}
},
{
"_index" : "example-index",
"_id" : "2",
"_score" : 0.5833334,
"_rank" : 2,
"_source" : {
"integer" : 2,
"vector" : [
4
],
"text" : "rrf rrf"
}
},
{
"_index" : "example-index",
"_id" : "4",
"_score" : 0.5,
"_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 |
これで、2つの個別にランク付けされた結果セットを取得し、rrf
リトリーバーのパラメータを使用してRRF式を適用して最終的なランク付けを取得できます。
Python
# 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式に基づいてドキュメントをランク付けし、rank_window_size
の5
でRRF結果セットの下部2
ドキュメントを切り捨てます。最終的に_id: 3
が_rank: 1
、_id: 2
が_rank: 2
、_id: 4
が_rank: 3
になります。このランク付けは、元のRRF検索からの結果セットと一致します。
RRFでの説明
個々のクエリスコアリングの詳細に加えて、explain=true
パラメータを使用して、各ドキュメントのRRFスコアがどのように計算されたかに関する情報を取得できます。上記の例を使用し、explain=true
を検索リクエストに追加することで、次のような応答が得られます:
Js
{
"hits":
[
{
"_index": "example-index",
"_id": "3",
"_score": 0.8333334,
"_rank": 1,
"_explanation":
{
"value": 0.8333334,
"description": "rrf score: [0.8333334] computed for initial ranks [2, 1] with rankConstant: [1] as sum of [1 / (rank + rankConstant)] for each query",
"details":
[
{
"value": 2,
"description": "rrf score: [0.33333334], for rank [2] in query at index [0] computed as [1 / (2 + 1]), for matching query with score: ",
"details":
[
{
"value": 0.15876243,
"description": "weight(text:rrf in 0) [PerFieldSimilarity], result of:",
"details":
[
...
]
}
]
},
{
"value": 1,
"description": "rrf score: [0.5], for rank [1] in query at index [1] computed as [1 / (1 + 1]), for matching query with score: ",
"details":
[
{
"value": 1,
"description": "within top k documents",
"details":
[]
}
]
}
]
}
}
...
]
}
_id=3 を持つドキュメントの最終RRFスコア |
|
このスコアが各個別のクエリにおけるこのドキュメントのランクに基づいて計算された方法の説明 | |
各クエリのRRFスコアがどのように計算されたかの詳細 | |
value はこのドキュメントの特定のクエリにおけるrank を指定します |
|
基本クエリの標準explain 出力、マッチする用語と重みを説明 |
|
value はこのドキュメントの2番目の(knn )クエリにおけるrank を指定します |
上記に加えて、RRFでの説明は_name
パラメータを使用してnamed queriesもサポートしています。named queriesを使用すると、複数のクエリを扱う際にRRFスコア計算の理解が容易で直感的になります。したがって、次のようになります:
Js
GET example-index/_search
{
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"term": {
"text": "rrf"
}
}
}
},
{
"knn": {
"field": "vector",
"query_vector": [3],
"k": 5,
"num_candidates": 5,
"_name": "my_knn_query"
}
}
],
"rank_window_size": 5,
"rank_constant": 1
}
},
"size": 3,
"aggs": {
"int_count": {
"terms": {
"field": "integer"
}
}
}
}
knn リトリーバーのための_name を指定します |
応答には、説明にnamed queryが含まれるようになります:
Js
{
"hits":
[
{
"_index": "example-index",
"_id": "3",
"_score": 0.8333334,
"_rank": 1,
"_explanation":
{
"value": 0.8333334,
"description": "rrf score: [0.8333334] computed for initial ranks [2, 1] with rankConstant: [1] as sum of [1 / (rank + rankConstant)] for each query",
"details":
[
{
"value": 2,
"description": "rrf score: [0.33333334], for rank [2] in query at index [0] computed as [1 / (2 + 1]), for matching query with score: ",
"details":
[
...
]
},
{
"value": 1,
"description": "rrf score: [0.5], for rank [1] in query [my_knn_query] computed as [1 / (1 + 1]), for matching query with score: ",
"details":
[
...
]
}
]
}
}
...
]
}
匿名のat index n の代わりに、named query my_knn_query への参照があります。 |
RRFでのページネーション
- `````from + size````` ≤ `````rank_window_size````` : 最終的な`````rrf`````ランク付け結果セットから`````results[from: from+size]`````ドキュメントを取得できます
- `````from + size````` > `````rank_window_size````` : 利用可能な`````rank_window_size`````サイズの結果セットの外にリクエストがあるため、0結果が返されます。
ここで重要なことは、`````rank_window_size`````が個々のクエリコンポーネントから見ることができるすべての結果であるため、ページネーションは一貫性を保証します。すなわち、`````rank_window_size`````が同じであれば、複数のページでドキュメントがスキップされたり重複したりすることはありません。`````rank_window_size`````が変更されると、同じランクでも結果の順序が変わる可能性があります。
上記のすべてを説明するために、`````queryA`````と`````queryB`````の2つのクエリとそのランク付けされたドキュメントを持つ次の簡略化された例を考えてみましょう:
#### Python
``````python
| queryA | queryB |
_id: | 1 | 5 |
_id: | 2 | 4 |
_id: | 3 | 3 |
_id: | 4 | 1 |
_id: | | 2 |
`
#### Python
``````python
# 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
]となり、rank_window_size == len(results)
のためにそれをページネートします。このシナリオでは、次のようになります:
from=0, size=2
はランク[1, 2]
のドキュメント[1
,4
]を返しますfrom=2, size=2
はランク[3, 4]
のドキュメント[2
,3
]を返しますfrom=4, size=2
はランク[5]
のドキュメント[5
]を返しますfrom=6, size=2
は、反復する結果がないため、空の結果セットを返します
さて、rank_window_size=2
があれば、[1, 2]
と[5, 4]
のドキュメントをそれぞれqueryA
とqueryB
のクエリに対して見ることができます。数学を解決すると、[3: end]
の位置にあるドキュメントの知識がないため、結果がわずかに異なることがわかります。
Python
# 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
]となり、トップrank_window_size
結果、すなわち[1
, 5
]にページネートできます。したがって、上記と同じパラメータの場合、次のようになります:
from=0, size=2
はランク[1, 2]
の[1
,5
]を返しますfrom=2, size=2
は、利用可能なrank_window_size
結果の外にあるため、空の結果セットを返します。