Decay Ranker の概要
従来のベクトル検索では、結果はベクトルの類似度、つまり数学空間でベクトルがどれだけ一致するかだけに基づいてランク付けされます。しかし、実際のアプリケーションでは、コンテンツが本当に関連しているかどうかは、意味的な類似度だけでは決まらないことがよくあります。
次のような日常的なシナリオを考えてみましょう。
-
昨日の記事が、3 年前の類似記事よりも上位にランク付けされるべきニュース検索
-
5 分の距離にある店舗を、30 分の移動を要する店舗よりも優先するレストラン検索
-
検索クエリとの類似度が多少低くても、トレンドの商品をブーストする E コマースプラットフォーム
これらのシナリオには共通のニーズがあります。それは、ベクトルの類似度と、時間、距離、人気度などの他の数値要因のバランスを取ることです。
Zilliz Cloud の Decay Ranker は、数値フィールドの値に基づいて検索ランキングを調整することで、このニーズに対応します。ベクトルの類似度と、データの「鮮度」「近さ」、その他の数値特性のバランスを取ることができ、より直感的で文脈に即した検索体験を実現します。
使用上の注意
-
Decay ランキングはグループ化検索では使用できません。
-
Decay ランキングに使用するフィールドは数値型(
INT8、INT16、INT32、INT64、FLOAT、DOUBLE)である必要があります。 -
各 Decay Ranker は、数値フィールドを 1 つだけ使用できます。
-
時間単位の一貫性: 時間ベースの Decay ランキングを使用する場合、
origin、scale、offsetパラメーターの単位は、コレクションデータで使用されている単位と一致している必要があります。-
コレクションがタイムスタンプを秒で保存している場合は、すべてのパラメーターに秒を使用します。
-
コレクションがタイムスタンプをミリ秒で保存している場合は、すべてのパラメーターにミリ秒を使用します。
-
コレクションがタイムスタンプをマイクロ秒で保存している場合は、すべてのパラメーターにマイクロ秒を使用します。
-
仕組み
Decay ランキングは、時間や地理的距離などの数値要因をランキングプロセスに組み込むことで、従来のベクトル検索を強化します。プロセス全体は次の段階に従います。
ステージ 1: 正規化された類似度スコアを計算する
まず、Zilliz Cloud はベクトルの類似度スコアを計算して正規化し、一貫した比較を行えるようにします。
-
L2 および JACCARD 距離メトリクス(値が小さいほど類似度が高い)の場合:
plaintextnormalized_score = 1.0 - (2 × arctan(score))/πこれにより、距離が 0~1 の類似度スコアに変換されます。値が大きいほど類似度が高くなります。
-
IP、COSINE、BM25 メトリクス(値が大きいほど一致度が高い)の場合: スコアは正規化せずにそのまま使用されます。
ステージ 2: 減衰スコアを計算する
次に、Zilliz Cloud は、選択した Decay Ranker を使用して、数値フィールドの値(タイムスタンプや距離など)に基づいて減衰スコアを計算します。
-
各 Decay Ranker は、生の数値を 0~1 の正規化された関連度スコアに変換します。
-
減衰スコアは、理想的なポイントからの「距離」に基づいて、アイテムがどれだけ関連しているかを表します。
具体的な計算式は、Decay Ranker の種類によって異なります。減衰スコアの計算方法の詳細については、Gaussian Decay、Exponential Decay、Linear Decay の各ページを参照してください。
ステージ 3: 最終スコアを計算する
最後に、Zilliz Cloud は正規化された類似度スコアと減衰スコアを組み合わせて、最終的なランキングスコアを生成します。
final_score = normalized_similarity_score × decay_score
ハイブリッド検索(複数のベクトルフィールドを組み合わせる検索)の場合、Zilliz Cloud は検索リクエスト間で最大の正規化類似度スコアを取得します。
final_score = max([normalized_score₁, normalized_score₂, ..., normalized_scoreₙ]) × decay_score
たとえば、ハイブリッド検索で論文のベクトル類似度スコアが 0.82、BM25 ベースのテキスト検索のスコアが 0.91 の場合、Zilliz Cloud は減衰係数を適用する前に 0.91 を基準の類似度スコアとして使用します。
実際の Decay ランキング
時間ベースの減衰を使用して 「AI 研究論文」 を検索する実用的なシナリオで、Decay ランキングを見てみましょう:
この例では、減衰スコアは時間の経過とともに関連度がどのように低下するかを反映しています。新しい論文ほど 1.0 に近いスコアを受け取り、古い論文ほど低いスコアを受け取ります。これらの値は、特定の Decay Ranker を使用して計算されます。詳細については、適切な Decay Ranker を選択する を参照してください。
| 論文 | ベクトル類似度 | 正規化類似度スコア | 公開日 | 減衰スコア | 最終スコア | 最終順位 |
|---|---|---|---|---|---|---|
| 論文 A | 高 | 0.85 (COSINE) | 2 週間前 | 0.80 | 0.68 | #2 |
| 論文 B | 非常に高い | 0.92 (COSINE) | 6 か月前 | 0.45 | 0.41 | #3 |
| 論文 C | 中程度 | 0.75 (COSINE) | 1 日前 | 0.98 | 0.74 | #1 |
| 論文 D | 中~高 | 0.76 (COSINE) | 3 週間前 | 0.70 | 0.53 | #4 |
Decay 再ランキングを行わない場合、論文 B は純粋なベクトル類似度(0.92)に基づいて最高位にランク付けされます。しかし、Decay 再ランキングを適用すると、次のようになります。
-
論文 C は中程度の類似度にもかかわらず、非常に新しい(昨日公開された)ため、1 位に浮上します。
-
論文 B は非常に高い類似度にもかかわらず、比較的古いため、3 位に下がります。
-
論文 D は L2 距離(値が小さいほど良い)を使用するため、減衰を適用する前にスコアが 1.2 から 0.76 に正規化されます。
適切な Decay Ranker を選択する
Zilliz Cloud は gauss、exp、linear という異なる Decay Ranker を提供しており、それぞれ特定のユースケース向けに設計されています。
Decay Ranker | 特性 | 理想的なユースケース | シナリオ例 |
|---|---|---|---|
Gaussian( | 自然な感覚で、適度に広がる緩やかな減少 |
| レストラン検索では、3 km 離れた良質な店舗も、近くの選択肢より下位ながら引き続き見つけることができます。 |
Exponential( | 初期は急速に減少しつつ、ロングテールを維持する性質 |
| ニュースアプリでは、昨日の記事は 1 週間前のコンテンツよりもはるかに上位にランク付けされますが、非常に関連性の高い古い記事も表示されることがあります。 |
Linear( | 明確なカットオフを伴う、一貫性があり予測可能な減少 |
| イベント検索では、2 週間より先のイベントはまったく表示されません。 |
各 Decay Ranker のスコア計算方法と具体的な減少パターンの詳細については、専用のドキュメントを参照してください。
実装例
Decay Ranker は、Zilliz Cloud の標準ベクトル検索とハイブリッド検索の両方に適用できます。以下に、この機能を実装するための主要なコードスニペットを示します。
Decay 関数を使用する前に、まず減衰計算に使用する適切な数値フィールド(タイムスタンプ、距離など)を含むコレクションを作成する必要があります。コレクションの設定、スキーマの定義、データの挿入を含む完全な動作例については、チュートリアル:Milvus で時間ベースのランキングを実装する を参照してください。
Decay Ranker を作成する
Decay ランキングを実装するには、まず適切な設定で Function オブジェクトを定義します。
- Python
- Java
- Go
- Rust
- C++
- NodeJS
- CSharp
- cURL
from pymilvus import Function, FunctionType
import datetime
# Create a decay function for timestamp-based decay
# Note: All time parameters must use the same unit as your collection data
rerank = Function(
name="time_decay", # Function identifier
input_field_names=["timestamp"], # Numeric field to use for decay
function_type=FunctionType.RERANK, # Must be set to RERANK for decay rankers
params={
"reranker": "decay", # Specify decay reranker. Must be "decay"
"function": "gauss", # Choose decay function type: "gauss", "exp", or "linear"
"origin": int(datetime.datetime(2025, 1, 15).timestamp()), # Reference point (seconds)
"scale": 7 * 24 * 60 * 60, # 7 days in seconds (must match collection data unit)
"offset": 24 * 60 * 60, # 1 day no-decay zone (must match collection data unit)
"decay": 0.5 # Half score at scale distance
}
)
import io.milvus.v2.service.vector.request.ranker.DecayRanker;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collections;
ZonedDateTime zdt = ZonedDateTime.of(2025, 1, 25, 0, 0, 0, 0, ZoneId.systemDefault());
DecayRanker rerank = DecayRanker.builder()
.name("time_decay")
.inputFieldNames(Collections.singletonList("timestamp"))
.function("gauss")
.origin(zdt.toInstant().toEpochMilli())
.scale(7 * 24 * 60 * 60)
.offset(24 * 60 * 60)
.decay(0.5)
.build();
import "github.com/milvus-io/milvus/client/v3/entity"
rerank := entity.NewFunction().
WithName("time_decay").
WithInputFields("timestamp").
WithType(entity.FunctionTypeRerank).
WithParam("reranker", "decay").
WithParam("function", "gauss").
WithParam("origin", 1735689600).
WithParam("scale", 7*24*60*60).
WithParam("offset", 24*60*60).
WithParam("decay", 0.5)
use milvus::v2::prelude::*;
let config = ConnectConfig::new().uri("YOUR_CLUSTER_ENDPOINT").token("YOUR_CLUSTER_TOKEN");
let client = ClientV2::new(&config).await?;
let rerank = DecayRerank::new()
.function(Function::new()
.name("time_decay")
.input_fields(["timestamp"])
.function_type(FunctionType::Rerank)
.param("reranker", "decay"))
.decay_function("gauss")
.origin(1735689600)
.scale(7 * 24 * 60 * 60)
.offset(24 * 60 * 60)
.decay(0.5);
auto rerank = std::make_shared<milvus::DecayRerank>("time_decay");
rerank->AddInputFieldName("timestamp");
rerank->SetFunction("gauss");
rerank->SetOrigin(1735689600);
rerank->SetScale(7 * 24 * 60 * 60);
rerank->SetOffset(24 * 60 * 60);
rerank->SetDecay(0.5);
import {FunctionType } from "@zilliz/milvus2-sdk-node";
const rerank = {
name: "time_decay",
input_field_names: ["timestamp"],
type: FunctionType.RERANK,
params: {
reranker: "decay",
function: "gauss",
origin: new Date(2025, 1, 15).getTime(),
scale: 7 * 24 * 60 * 60,
offset: 24 * 60 * 60,
decay: 0.5,
},
};
using Milvus.Client.V2.Types;
// Create a decay function for timestamp-based decay
// Note: All time parameters must use the same unit as your collection data
var rerank = new DecayRerank("time_decay") // Function identifier
.WithInputField("timestamp") // Numeric field to use for decay
.WithFunction("gauss") // Choose decay function type: "gauss", "exp", or "linear"
.WithOrigin(new DateTimeOffset(2025, 1, 15, 0, 0, 0, TimeSpan.Zero).ToUnixTimeSeconds()) // Reference point (seconds)
.WithScale(7 * 24 * 60 * 60) // 7 days in seconds (must match collection data unit)
.WithOffset(24 * 60 * 60) // 1 day no-decay zone (must match collection data unit)
.WithDecay(0.5f); // Half score at scale distance
# Note: The RESTful API does not expose the decay/rerank operation as of Milvus v3.0.x.
パラメーター | 必須 | 説明 | Value/Example |
|---|---|---|---|
| はい | 検索を実行するときに使用する関数の識別子です。ユースケースに関連するわかりやすい名前を選択してください。 |
|
| はい | 減衰スコアの計算に使用する数値フィールドです。減衰の計算に使用するデータ属性を決定します(例:時間ベースの減衰ではタイムスタンプ、位置ベースの減衰では座標)。 関連する数値を含むコレクション内のフィールドである必要があります。INT8/16/32/64, FLOAT、DOUBLE をサポートします。 |
|
| はい | 作成する関数の種類を指定します。 すべての Decay Ranker で |
|
| はい | 使用する再ランキング方法を指定します。 Decay ランキング機能を有効にするには、 |
|
| はい | 適用する数学的な Decay Ranker を指定します。関連度の低下曲線の形状を決定します。 適切な関数の選択についてのガイダンスは、適切な Decay Ranker を選択するセクションを参照してください。 |
|
| はい | 減衰スコアの計算基準となる参照ポイントです。この値を持つアイテムは最大の関連度スコアを受け取ります。 時間ベースの減衰では、時間単位がコレクションデータと一致している必要があります。 |
|
| はい | 関連度が 時間ベースの減衰では、時間単位がコレクションデータと一致している必要があります。 値が大きいほど関連度の低下が緩やかになり、値が小さいほど低下が急になります。 |
|
| いいえ |
時間ベースの減衰では、時間単位がコレクションデータと一致している必要があります。
|
|
| いいえ |
0 から 1 の間である必要があります。 |
|
標準ベクトル検索に適用する
Decay Ranker を定義したら、ranker パラメーターに渡すことで検索操作中に適用できます。
- Python
- Java
- Go
- Rust
- C++
- NodeJS
- CSharp
- cURL
# Use the decay function in standard vector search
results = milvus_client.search(
collection_name,
data=[your_query_vector], # Replace with your query vector
anns_field="vector_field",
limit=10,
output_fields=["document", "timestamp"], # Include the decay field in outputs to see values
ranker=rerank, # Apply the decay ranker here
consistency_level="Strong"
)
import io.milvus.v2.service.vector.request.FunctionScore;
import io.milvus.v2.service.vector.request.SearchReq;
import io.milvus.v2.service.vector.request.data.EmbeddedText;
import io.milvus.v2.service.vector.response.SearchResp;
import java.util.Arrays;
import java.util.Collections;
SearchReq searchReq = SearchReq.builder()
.collectionName(COLLECTION_NAME)
.data(Collections.singletonList(new EmbeddedText("search query")))
.annsField("vector_field")
.limit(10)
.outputFields(Arrays.asList("document", "timestamp"))
.functionScore(FunctionScore.builder()
.addFunction(rerank)
.build())
.build();
SearchResp searchResp = client.search(searchReq);
import (
"fmt"
"github.com/milvus-io/milvus/client/v3/entity"
"github.com/milvus-io/milvus/client/v3/milvusclient"
)
functionScore := entity.NewFunctionScore().AddFunction(rerank)
resultSets, err := client.Search(ctx, milvusclient.NewSearchOption(
collection_name, // collection name
10, // limit
[]entity.Vector{your_query_vector}, // query vector
).WithANNSField("vector_field").
WithFunctionScore(functionScore).
WithOutputFields("document", "timestamp"))
if err != nil {
fmt.Println(err.Error())
// handle error
}
use milvus::v2::prelude::*;
let function_score = FunctionScore::new().add_function(rerank);
let request = SearchRequest::builder()
.collection_name(collection_name)
.vectors(SearchVectors::Float(vec![your_query_vector]))
.vector_field("vector_field")
.limit(10)
.output_fields(vec!["document", "timestamp"])
.rerank(function_score)
.build()?;
let resp = client.search(request).await?;
auto function_score = std::make_shared<milvus::FunctionScore>();
function_score->AddFunction(rerank);
auto request = milvus::SearchRequest()
.WithCollectionName(collection_name)
.WithAnnsField("dense")
.WithRerank(function_score)
.AddOutputField("document")
.AddOutputField("timestamp")
.AddFloatVector(your_query_vector);
milvus::SearchResponse response;
status = client->Search(request, response);
if (!status.IsOk()) {
std::cout << status.Message() << std::endl;
}
const result = await milvusClient.search({
collection_name: collection_name,
data: [your_query_vector], // Replace with your query vector
anns_field: "dense",
limit: 10,
output_fields: ["document", "timestamp"],
rerank: rerank,
consistency_level: "Strong",
});
using Milvus.Client.V2;
using Milvus.Client.V2.Requests.Dql;
using Milvus.Client.V2.Responses.Dql;
using Milvus.Client.V2.Types;
var config = new ConnectConfig
{
Uri = "YOUR_CLUSTER_ENDPOINT"
};
using var client = new MilvusClientV2(config);
await client.ConnectAsync();
// Use the decay function in standard vector search
SearchResp results = await client.SearchAsync(new SearchReq
{
CollectionName = collectionName,
Vectors = [yourQueryVector], // Replace with your query vector
VectorFieldName = "vector_field",
Limit = 10,
Parameters = new SearchParameters
{
OutputFields = { "document", "timestamp" }, // Include the decay field in outputs to see values
FunctionScoreReranker = rerank, // Apply the decay ranker here
ConsistencyLevel = ConsistencyLevel.Strong
}
});
# Note: The RESTful API does not expose the decay/rerank operation as of Milvus v3.0.x.