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: Decay スコアを計算する
次に、Zilliz Cloud は、選択した Decay Ranker を使用して、数値フィールドの値(タイムスタンプや距離など)に基づいて Decay スコアを計算します。
-
各 Decay Ranker は、生の数値を 0-1 の正規化された関連性スコアに変換します
-
Decay スコアは、理想的なポイントからの「距離」に基づいて項目がどれだけ関連しているかを表します
具体的な計算式は Decay Ranker のタイプによって異なります。Decay スコアの計算方法の詳細については、Gaussian Decay、Exponential Decay、Linear Decay の専用ページを参照してください。
ステップ 3: 最終スコアを計算する
最後に、Zilliz Cloud は正規化された類似度スコアと Decay スコアを組み合わせて、最終的なランキングスコアを生成します。
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 は Decay 係数を適用する前の基準類似度スコアとして 0.91 を使用します。
Decay ランキングの実例
実際のシナリオで Decay ランキングを見てみましょう。ここでは、時間ベースの Decay を使用して "AI research papers" を検索します。
この例では、Decay スコアは時間の経過とともに関連性がどれだけ低下するかを表します。新しい論文ほど 1.0 に近いスコアを受け取り、古い論文ほど低いスコアを受け取ります。これらの値は特定の Decay Ranker を使用して計算されます。詳細については、適切な Decay Ranker を選択する を参照してください。
| 論文 | ベクトル類似度 | 正規化類似度スコア | 公開日 | Decay スコア | 最終スコア | 最終ランク |
|---|---|---|---|---|---|---|
| 論文 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 リランキングを行わない場合、純粋なベクトル類似度(0.92)に基づくと論文 B が最も上位にランク付けされます。しかし、Decay リランキングを適用すると次のようになります。
-
論文 C は、類似度が中程度であるにもかかわらず、非常に新しい(昨日公開)ため、1 位に浮上します
-
論文 B は、類似度が非常に高いにもかかわらず、比較的古いため、3 位に下落します
-
論文 D は L2 距離(値が小さいほど良い)を使用するため、Decay を適用する前にスコアが 1.2 から 0.76 に正規化されます
適切な Decay Ranker を選択する
Zilliz Cloud では、それぞれ特定のユースケース向けに設計された個別の Decay Ranker(gauss、exp、linear)を提供しています。
Decay Ranker | 特性 | 理想的なユースケース | シナリオ例 |
|---|---|---|---|
Gaussian( | 自然に感じられる緩やかな低下が、ほどよく広範囲に及ぶ |
| レストラン検索では、3 km 先の優良店舗も、近くの候補より下位にはなるものの、引き続き発見できます |
Exponential( | 最初は急速に低下しますが、長い裾を維持する |
| ニュースアプリでは、昨日の記事が 1 週間前のコンテンツよりはるかに上位にランク付けされますが、非常に関連性の高い古い記事も引き続き表示される可能性があります |
Linear( | 明確なカットオフを伴う、一貫性があり予測可能な低下 |
| イベント検索では、2 週間より先のイベントはまったく表示されません |
各 Decay Ranker がスコアを計算する方法と、具体的な低下パターンの詳細については、専用のドキュメントを参照してください。
実装例
Decay Ranker は、Zilliz Cloud の標準ベクトル検索とハイブリッド検索の両方に適用できます。以下に、この機能を実装するための主要なコードスニペットを示します。
Decay 関数を使用する前に、まず Decay 計算に使用する適切な数値フィールド(タイムスタンプ、距離など)を含むコレクションを作成する必要があります。コレクションのセットアップ、スキーマ定義、データ挿入を含む完全な動作例については、Tutorial: Implement Time-based Ranking in Milvus を参照してください。
Decay Ranker を作成する
Decay ランキングを実装するには、まず適切な構成で Function オブジェクトを定義します。
- Python
- Java
- Go
- Rust
- C++
- NodeJS
- 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;
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();
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::*;
#[tokio::main]
async fn main() -> Result<()> {
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);
Ok(())
}
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"],
function_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,
},
};
# Note: The RESTful API does not expose the decay/rerank operation as of Milvus v3.0.x.
パラメーター | 必須 | 説明 | 値/Example |
|---|---|---|---|
| はい | 検索の実行時に使用する関数の識別子です。ユースケースに合ったわかりやすい名前を選択してください。 |
|
| はい | Decay スコアの計算に使用する数値フィールドです。Decay の計算に使用するデータ属性を決定します(例: 時間ベースの Decay ではタイムスタンプ、位置ベースの Decay では座標)。 関連する数値を含む、コレクション内のフィールドである必要があります。INT8/16/32/64, FLOAT, DOUBLE をサポートします。 |
|
| はい | 作成する関数のタイプを指定します。 すべての Decay Ranker で |
|
| はい | 使用するリランキング方法を指定します。 Decay ランキング機能を有効にするには、 |
|
| はい | 適用する数学的な Decay Ranker を指定します。関連性の低下の曲線形状を決定します。 適切な関数の選択については、適切な Decay Ranker を選択するセクションを参照してください。 |
|
| はい | Decay スコアの計算の基準となる参照点です。この値にある項目は最大の関連性スコアを受け取ります。 時間ベースの Decay では、時間単位をコレクションデータと一致させる必要があります。 |
|
| はい | 関連性が 時間ベースの Decay では、時間単位をコレクションデータと一致させる必要があります。 値が大きいほど関連性の低下が緩やかになり、値が小さいほど低下が急になります。 |
|
| いいえ |
時間ベースの Decay では、時間単位をコレクションデータと一致させる必要があります。
|
|
| いいえ |
0 から 1 の間である必要があります。 |
|
標準ベクトル検索に適用する
Decay Ranker を定義したら、検索操作時に ranker パラメーターに渡すことで適用できます。
- Python
- Java
- Go
- Rust
- C++
- NodeJS
- 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.SearchReq;
import io.milvus.v2.service.vector.response.SearchResp;
import io.milvus.v2.service.vector.request.data.EmbeddedText;
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);
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
}
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",
});
# Note: The RESTful API does not expose the decay/rerank operation as of Milvus v3.0.x.