搜索相关性
默认情况下,Qdrant 根据向量相似度分数对搜索结果进行排名。但是,您可能希望在排名结果时考虑其他因素。Qdrant 提供了多种工具来帮助您实现这一目标。
分数加权 (Score Boosting)
自 v1.14.0 版本起可用
在将向量搜索引入特定应用程序时,有时需要考虑业务逻辑来对最终结果列表进行排名。
一个简单的例子是我们自己的文档搜索栏。它为文档网站的每一部分都有相应的向量。如果仅“单纯”使用向量进行搜索,各种元素都会被视为同样好的结果。然而,在搜索文档时,我们可以建立一个重要性层级:
标题 > 内容 > 代码片段
解决此问题的一种方法是根据元素类型对结果进行加权。例如,我们可以为标题和内容分配更高的权重,而让代码片段保持不加权。
伪代码如下所示:
score = score + (is_title * 0.5) + (is_content * 0.25)
查询 API 可以根据以下内容使用自定义公式对点进行重评分 (Rescore):
- 动态负载 (Payload) 值
- 条件
- 预取 (Prefetch) 分数
为了表示该公式,语法使用对象来标识每个元素。以文档搜索为例,请求如下所示:
POST /collections/{collection_name}/points/query
{
"prefetch": {
"query": [0.2, 0.8, ...], // <-- dense vector
"limit": 50
},
"query": {
"formula": {
"sum": [
"$score",
{
"mult": [
0.5,
{
"key": "tag",
"match": { "any": ["h1", "h2", "h3", "h4"] }
}
]
},
{
"mult": [
0.25,
{
"key": "tag",
"match": { "any": ["p", "li"] }
}
]
}
]
}
}
}
from qdrant_client import QdrantClient, models
tag_boosted = client.query_points(
collection_name="{collection_name}",
prefetch=models.Prefetch(
query=[0.1, 0.45, 0.67], # <-- dense vector
limit=50
),
query=models.FormulaQuery(
formula=models.SumExpression(sum=[
"$score",
models.MultExpression(mult=[0.5, models.FieldCondition(key="tag", match=models.MatchAny(any=["h1", "h2", "h3", "h4"]))]),
models.MultExpression(mult=[0.25, models.FieldCondition(key="tag", match=models.MatchAny(any=["p", "li"]))])
]
))
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ host: "localhost", port: 6333 });
const tag_boosted = await client.query("{collection_name}", {
prefetch: {
query: [0.2, 0.8, 0.1, 0.9],
limit: 50
},
query: {
formula: {
sum: [
"$score",
{
mult: [ 0.5, { key: "tag", match: { any: ["h1", "h2", "h3", "h4"] }} ]
},
{
mult: [ 0.25, { key: "tag", match: { any: ["p", "li"] }} ]
}
]
}
}
});
use qdrant_client::qdrant::{
Condition, Expression, FormulaBuilder, PrefetchQueryBuilder, QueryPointsBuilder,
};
use qdrant_client::Qdrant;
let client = Qdrant::from_url("https://:6334").build()?;
let _tag_boosted = client.query(
QueryPointsBuilder::new("{collection_name}")
.add_prefetch(PrefetchQueryBuilder::default()
.query(vec![0.01, 0.45, 0.67])
.limit(100u64)
)
.query(FormulaBuilder::new(Expression::sum_with([
Expression::score(),
Expression::mult_with([
Expression::constant(0.5),
Expression::condition(Condition::matches("tag", ["h1", "h2", "h3", "h4"])),
]),
Expression::mult_with([
Expression::constant(0.25),
Expression::condition(Condition::matches("tag", ["p", "li"])),
]),
])))
.limit(10)
).await?;
import static io.qdrant.client.ConditionFactory.matchKeywords;
import static io.qdrant.client.ExpressionFactory.condition;
import static io.qdrant.client.ExpressionFactory.constant;
import static io.qdrant.client.ExpressionFactory.mult;
import static io.qdrant.client.ExpressionFactory.sum;
import static io.qdrant.client.ExpressionFactory.variable;
import static io.qdrant.client.QueryFactory.formula;
import static io.qdrant.client.QueryFactory.nearest;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.Formula;
import io.qdrant.client.grpc.Points.MultExpression;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;
import io.qdrant.client.grpc.Points.SumExpression;
import java.util.List;
QdrantClient client =
new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client
.queryAsync(
QueryPoints.newBuilder()
.setCollectionName("{collection_name}")
.addPrefetch(
PrefetchQuery.newBuilder()
.setQuery(nearest(0.01f, 0.45f, 0.67f))
.setLimit(100)
.build())
.setQuery(
formula(
Formula.newBuilder()
.setExpression(
sum(
SumExpression.newBuilder()
.addSum(variable("$score"))
.addSum(
mult(
MultExpression.newBuilder()
.addMult(constant(0.5f))
.addMult(
condition(
matchKeywords(
"tag",
List.of("h1", "h2", "h3", "h4"))))
.build()))
.addSum(mult(MultExpression.newBuilder()
.addMult(constant(0.25f))
.addMult(
condition(
matchKeywords(
"tag",
List.of("p", "li"))))
.build()))
.build()))
.build()))
.build())
.get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.QueryAsync(
collectionName: "{collection_name}",
prefetch:
[
new PrefetchQuery { Query = new float[] { 0.01f, 0.45f, 0.67f }, Limit = 100 },
],
query: new Formula
{
Expression = new SumExpression
{
Sum =
{
"$score",
new MultExpression
{
Mult = { 0.5f, Match("tag", ["h1", "h2", "h3", "h4"]) },
},
new MultExpression { Mult = { 0.25f, Match("tag", ["p", "li"]) } },
},
},
},
limit: 10
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Query(context.Background(), &qdrant.QueryPoints{
CollectionName: "{collection_name}",
Prefetch: []*qdrant.PrefetchQuery{
{
Query: qdrant.NewQuery(0.01, 0.45, 0.67),
},
},
Query: qdrant.NewQueryFormula(&qdrant.Formula{
Expression: qdrant.NewExpressionSum(&qdrant.SumExpression{
Sum: []*qdrant.Expression{
qdrant.NewExpressionVariable("$score"),
qdrant.NewExpressionMult(&qdrant.MultExpression{
Mult: []*qdrant.Expression{
qdrant.NewExpressionConstant(0.5),
qdrant.NewExpressionCondition(qdrant.NewMatchKeywords("tag", "h1", "h2", "h3", "h4")),
},
}),
qdrant.NewExpressionMult(&qdrant.MultExpression{
Mult: []*qdrant.Expression{
qdrant.NewExpressionConstant(0.25),
qdrant.NewExpressionCondition(qdrant.NewMatchKeywords("tag", "p", "li")),
},
}),
},
}),
}),
})
有多种表达式可用。请查看 API 文档了解具体细节。
- constant (常量) - 一个浮点数,例如
0.5。 "$score"- 引用预取中点的分数。这与"$score[0]"相同。"$score[0]","$score[1]","$score[2]", … - 使用多个预取时,可以通过预取数组中的索引引用特定的预取。- payload key (负载键) - 任何纯字符串都将指向负载键。这使用了在其他地方使用的 jsonpath 格式,例如
key或key.subkey。它将尝试从给定的键中提取数字。 - condition (条件) - 过滤条件。如果满足条件,则为
1.0,否则为0.0。 - mult (乘法) - 将表达式数组相乘。
- sum (求和) - 将表达式数组相加。
- div (除法) - 将一个表达式除以另一个表达式。
- abs (绝对值) - 表达式的绝对值。
- pow (幂运算) - 将一个表达式的幂提升为另一个表达式。
- sqrt (平方根) - 表达式的平方根。
- log10 - 表达式的以 10 为底的对数。
- ln - 表达式的自然对数。
- exp (指数函数) - 表达式的指数函数 (
e^x)。 - geo distance (地理距离) - 两个地理点之间的半正矢距离 (Haversine distance)。值必须是
{ "lat": 0.0, "lon": 0.0 }对象。 - decay (衰减) - 对表达式应用衰减函数,将输出限制在 0 和 1 之间。可用的衰减函数包括 线性 (linear)、指数 (exponential) 和 高斯 (gaussian)。查看更多。
- datetime (日期时间) - 解析日期时间字符串(参见此处的格式),并将其用作 POSIX 时间戳(秒)。
- datetime key - 指定某个负载键包含待解析为 POSIX 秒数的日期时间字符串。
当找不到变量(来自负载或预取分数)时,可以定义一个默认值。这是以变量到值的映射形式给出的。如果没有变量且没有定义默认值,则使用默认值 0.0。
衰减函数 (Decay Functions)
衰减函数使您能够根据值与目标值的距离,使用线性、指数或高斯衰减函数来修改分数。对于所有衰减函数,以下是可用的参数:
| 参数 | 默认 | 描述 |
|---|---|---|
x | 不适用 | 要衰减的值 |
target (目标值) | 0.0 | 衰减达到峰值时的值。对于距离,通常设置为 0.0,但可以设置为任何值。 |
scale (比例) | 1.0 | 衰减函数等于 midpoint 时的值。这是以 x 单位计算的。例如,如果 x 是以米为单位的,则 scale 为 5000 意味着 5 公里。必须是非零正数。 |
midpoint (中点) | 0.5 | 当 x 等于 target ± scale 时,输出为 midpoint。必须在 (0.0, 1.0) 范围内(不包含边界)。 |

每个衰减函数的公式如下:
| 衰减函数 | 范围 | 公式 | |
|---|---|---|---|
lin_decay | [0, 1] | $\text{lin_decay}(x) = \max\left(0,\ -\frac{(1-m_{idpoint})}{s_{cale}}\cdot {abs}(x-t_{arget})+1\right)$ | |
exp_decay | (0, 1] | $\text{exp_decay}(x) = \exp\left(\frac{\ln(m_{idpoint})}{s_{cale}}\cdot {abs}(x-t_{arget})\right)$ | |
gauss_decay | (0, 1] | $\text{gauss_decay}(x) = \exp\left(\frac{\ln(m_{idpoint})}{s_{cale}^{2}}\cdot (x-t_{arget})^{2}\right)$ |
提升离用户更近的点
衰减函数的一个示例是将分数与结果离用户的距离结合起来。
考虑到每个点都有关联的地理位置,我们可以计算该点与请求位置之间的距离。
假设我们在预取中有余弦相似度分数,我们可以使用辅助函数通过衰减函数将地理距离限制在 0 和 1 之间。一旦限制,我们就可以将分数和距离相加。伪代码如下:
score = score + gauss_decay(distance)
在这种情况下,我们使用 gauss_decay 函数。
POST /collections/{collection_name}/points/query
{
"prefetch": { "query": [0.2, 0.8, ...], "limit": 50 },
"query": {
"formula": {
"sum": [
"$score",
{
"gauss_decay": {
"x": {
"geo_distance": {
"origin": { "lat": 52.504043, "lon": 13.393236 },
"to": "geo.location"
}
},
"scale": 5000 // 5km
}
}
]
},
"defaults": { "geo.location": {"lat": 48.137154, "lon": 11.576124} }
}
}
from qdrant_client import QdrantClient, models
geo_boosted = client.query_points(
collection_name="{collection_name}",
prefetch=models.Prefetch(
query=[0.1, 0.45, 0.67], # <-- dense vector
limit=50
),
query=models.FormulaQuery(
formula=models.SumExpression(sum=[
"$score",
models.GaussDecayExpression(
gauss_decay=models.DecayParamsExpression(
x=models.GeoDistance(
geo_distance=models.GeoDistanceParams(
origin=models.GeoPoint(
lat=52.504043,
lon=13.393236
), # Berlin
to="geo.location"
)
),
scale=5000 # 5km
)
)
]),
defaults={"geo.location": models.GeoPoint(lat=48.137154, lon=11.576124)} # Munich
)
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ host: "localhost", port: 6333 });
const distance_boosted = await client.query("{collection_name}", {
prefetch: {
query: [0.1, 0.45, 0.67],
limit: 50
},
query: {
formula: {
sum: [
"$score",
{
gauss_decay: {
x: {
geo_distance: {
origin: { lat: 52.504043, lon: 13.393236 }, // Berlin
to: "geo.location"
}
},
scale: 5000 // 5km
}
}
]
},
defaults: { "geo.location": { lat: 48.137154, lon: 11.576124 } } // Munich
}
});
use qdrant_client::qdrant::{
GeoPoint, DecayParamsExpressionBuilder, Expression, FormulaBuilder, PrefetchQueryBuilder, QueryPointsBuilder,
};
use qdrant_client::Qdrant;
let client = Qdrant::from_url("https://:6334").build()?;
let _geo_boosted = client.query(
QueryPointsBuilder::new("{collection_name}")
.add_prefetch(
PrefetchQueryBuilder::default()
.query(vec![0.01, 0.45, 0.67])
.limit(100u64),
)
.query(
FormulaBuilder::new(Expression::sum_with([
Expression::score(),
Expression::exp_decay(
DecayParamsExpressionBuilder::new(Expression::geo_distance_with(
// Berlin
GeoPoint { lat: 52.504043, lon: 13.393236 },
"geo.location",
))
.scale(5_000.0),
),
]))
// Munich
.add_default("geo.location", GeoPoint { lat: 48.137154, lon: 11.576124 }),
)
.limit(10),
)
.await?;
import static io.qdrant.client.ExpressionFactory.expDecay;
import static io.qdrant.client.ExpressionFactory.geoDistance;
import static io.qdrant.client.ExpressionFactory.sum;
import static io.qdrant.client.ExpressionFactory.variable;
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.QueryFactory.formula;
import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.ValueFactory.value;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Common.GeoPoint;
import io.qdrant.client.grpc.Points.DecayParamsExpression;
import io.qdrant.client.grpc.Points.Formula;
import io.qdrant.client.grpc.Points.GeoDistance;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;
import io.qdrant.client.grpc.Points.SumExpression;
import java.util.Map;
QdrantClient client =
new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client
.queryAsync(
QueryPoints.newBuilder()
.setCollectionName("{collection_name}")
.addPrefetch(
PrefetchQuery.newBuilder()
.setQuery(nearest(0.01f, 0.45f, 0.67f))
.setLimit(100)
.build())
.setQuery(
formula(
Formula.newBuilder()
.setExpression(
sum(
SumExpression.newBuilder()
.addSum(variable("$score"))
.addSum(
expDecay(
DecayParamsExpression.newBuilder()
.setX(
geoDistance(
GeoDistance.newBuilder()
.setOrigin(
GeoPoint.newBuilder()
.setLat(52.504043)
.setLon(13.393236)
.build())
.setTo("geo.location")
.build()))
.setScale(5000)
.build()))
.build()))
.putDefaults(
"geo.location",
value(
Map.of(
"lat", value(48.137154),
"lon", value(11.576124))))
.build()))
.build())
.get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Expression;
var client = new QdrantClient("localhost", 6334);
await client.QueryAsync(
collectionName: "{collection_name}",
prefetch:
[
new PrefetchQuery { Query = new float[] { 0.01f, 0.45f, 0.67f }, Limit = 100 },
],
query: new Formula
{
Expression = new SumExpression
{
Sum =
{
"$score",
FromExpDecay(
new()
{
X = new GeoDistance
{
Origin = new GeoPoint { Lat = 52.504043, Lon = 13.393236 },
To = "geo.location",
},
Scale = 5000,
}
),
},
},
Defaults =
{
["geo.location"] = new Dictionary<string, Value>
{
["lat"] = 48.137154,
["lon"] = 11.576124,
},
},
}
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Query(context.Background(), &qdrant.QueryPoints{
CollectionName: "{collection_name}",
Prefetch: []*qdrant.PrefetchQuery{
{
Query: qdrant.NewQuery(0.2, 0.8),
},
},
Query: qdrant.NewQueryFormula(&qdrant.Formula{
Expression: qdrant.NewExpressionSum(&qdrant.SumExpression{
Sum: []*qdrant.Expression{
qdrant.NewExpressionVariable("$score"),
qdrant.NewExpressionExpDecay(&qdrant.DecayParamsExpression{
X: qdrant.NewExpressionGeoDistance(&qdrant.GeoDistance{
Origin: &qdrant.GeoPoint{
Lat: 52.504043,
Lon: 13.393236,
},
To: "geo.location",
}),
}),
},
}),
Defaults: qdrant.NewValueMap(map[string]any{
"geo.location": map[string]any{
"lat": 48.137154,
"lon": 11.576124,
},
}),
}),
})
基于时间的加权评分
或者将分数与结果的“新鲜度”信息结合起来。这适用于(新闻)文章,以及通常许多其他类型的搜索(想想你在应用程序中使用的“最新”过滤器)。
要实现基于时间的加权评分,您需要每个点在其负载中有一个日期时间字段,例如项目上传或上次更新的时间。然后我们可以计算此负载值与当前时间(我们的 target)之间的秒数差。
使用指数衰减函数(非常适合时间使用场景,因为新鲜度会迅速丧失),我们可以将此时间差转换为 0 到 1 之间的值,然后将其加到原始分数中,以优先显示最新结果。
score = score + exp_decay(current_time - point_time)
对于一个应用程序,如果结果在 1 天后开始变得只有一半相关(因此分数为 0.5),情况将如下所示:
POST /collections/{collection_name}/points/query
{
"prefetch": {
"query": [0.2, 0.8, ...], // <-- dense vector
"limit": 50
},
"query": {
"formula": {
"sum": [
"$score", // the final score = score + exp_decay(target_time - x_time)
{
"exp_decay": {
"x": {
"datetime_key": "update_time" // payload key
},
"target": {
"datetime": "YYYY-MM-DDT00:00:00Z" // current datetime
},
"scale": 86400, // 1 day in seconds
"midpoint": 0.5 // if item's "update_time" is more than 1 day apart from current datetime, relevance score is less than 0.5
}
}
]
}
}
}
from qdrant_client import QdrantClient, models
time_boosted = client.query_points(
collection_name="{collection_name}",
prefetch=models.Prefetch(
query=[0.1, 0.45, 0.67], # <-- dense vector
limit=50
),
query=models.FormulaQuery(
formula=models.SumExpression(
sum=[
"$score", # the final score = score + exp_decay(target_time - x_time)
models.ExpDecayExpression(
exp_decay=models.DecayParamsExpression(
x=models.DatetimeKeyExpression(
datetime_key="upload_time" # payload key
),
target=models.DatetimeExpression(
datetime="YYYY-MM-DDT00:00:00Z" # current datetime
),
scale=86400, # 1 day in seconds
midpoint=0.5 # if item's "update_time" is more than 1 day apart from current datetime, relevance score is less than 0.5
)
)
]
)
)
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ host: "localhost", port: 6333 });
const time_boosted = await client.query('collectionName', {
prefetch: {
query: [0.1, 0.45, 0.67], // <-- dense vector
limit: 50
},
query: {
formula: {
sum: [ // the final score = score + exp_decay(target_time - x_time)
"$score",
{
exp_decay: {
x: {
datetime_key: "update_time" // payload key
},
target: {
datetime: "YYYY-MM-DDT00:00:00Z" // current datetime
},
midpoint: 0.5,
scale: 86400 // 1 day in seconds
}
}
]
}
}
});
use qdrant_client::qdrant::{
DecayParamsExpressionBuilder, Expression, FormulaBuilder, PrefetchQueryBuilder, QueryPointsBuilder,
};
use qdrant_client::Qdrant;
let client = Qdrant::from_url("https://:6334").build()?;
let _geo_boosted = client.query(
QueryPointsBuilder::new("{collection_name}")
.add_prefetch(
PrefetchQueryBuilder::default()
.query(vec![0.1, 0.45, 0.67]) // <-- dense vector
.limit(50u64),
)
.query(
FormulaBuilder::new(Expression::sum_with([ // the final score = score + exp_decay(target_time - x_time)
Expression::score(),
Expression::exp_decay(
DecayParamsExpressionBuilder::new(Expression::datetime_key("update_time")) // payload key
.target(Expression::datetime("YYYY-MM-DDT00:00:00Z"))
.midpoint(0.5)
.scale(86400.0), // 1 day in seconds
),
]))
)
)
.await?;
import static io.qdrant.client.ExpressionFactory.datetime;
import static io.qdrant.client.ExpressionFactory.datetimeKey;
import static io.qdrant.client.ExpressionFactory.expDecay;
import static io.qdrant.client.ExpressionFactory.sum;
import static io.qdrant.client.ExpressionFactory.variable;
import static io.qdrant.client.QueryFactory.formula;
import static io.qdrant.client.QueryFactory.nearest;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.DecayParamsExpression;
import io.qdrant.client.grpc.Points.Formula;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;
import io.qdrant.client.grpc.Points.ScoredPoint;
import io.qdrant.client.grpc.Points.SumExpression;
import java.util.List;
QdrantClient client =
new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
List<ScoredPoint> time_boosted = client.queryAsync(
QueryPoints.newBuilder()
.setCollectionName("{collection_name}")
.addPrefetch(
PrefetchQuery.newBuilder()
.setQuery(nearest(0.1f, 0.45f, 0.67f)) // <-- dense vector
.setLimit(50)
.build())
.setQuery(
formula(
Formula.newBuilder()
.setExpression(
sum( // the final score = score + exp_decay(target_time - x_time)
SumExpression.newBuilder()
.addSum(variable("$score"))
.addSum(
expDecay(
DecayParamsExpression.newBuilder()
.setX(
datetimeKey("update_time")) // payload key
.setTarget(
datetime("YYYY-MM-DDT00:00:00Z")) // current datetime
.setMidpoint(0.5f)
.setScale(86400) // 1 day in seconds
.build()))
.build()))
.build()))
.build()
).get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
var client = new QdrantClient("localhost", 6334);
await client.QueryAsync(
collectionName: "{collection_name}",
prefetch:
[
new PrefetchQuery {
Query = new float[] { 0.1f, 0.45f, 0.67f }, // <-- dense vector
Limit = 50
},
],
query: new Formula
{
Expression = new SumExpression
{
Sum = // the final score = score + exp_decay(target_time - x_time)
{
"$score",
Expression.FromExpDecay(
new()
{
X = Expression.FromDateTimeKey("update_time"), // payload key
Target = Expression.FromDateTime("YYYY-MM-DDT00:00:00Z"), // current datetime
Midpoint = 0.5f,
Scale = 86400 // 1 day in seconds
}
)
}
}
}
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Query(context.Background(), &qdrant.QueryPoints{
CollectionName: "{collection_name}",
Prefetch: []*qdrant.PrefetchQuery{
{
Query: qdrant.NewQuery(0.1, 0.45, 0.67), // <-- dense vector
Limit: qdrant.PtrOf(uint64(50)),
},
},
Query: qdrant.NewQueryFormula(&qdrant.Formula{
Expression: qdrant.NewExpressionSum(&qdrant.SumExpression{
Sum: []*qdrant.Expression{ // the final score = score + exp_decay(target_time - x_time)
qdrant.NewExpressionVariable("$score"),
qdrant.NewExpressionExpDecay(&qdrant.DecayParamsExpression{
X: qdrant.NewExpressionDatetimeKey("update_time"), // payload key
Target: qdrant.NewExpressionDatetime("YYYY-MM-DDT00:00:00Z"), // current datetime
Scale: qdrant.PtrOf(float32(86400)), // 1 day in seconds
Midpoint: qdrant.PtrOf(float32(0.5)),
}),
},
}),
}),
})
最大边缘相关性 (MMR)
从 v1.15.0 版本开始可用
最大边缘相关性 (MMR) 是一种用于提高结果多样性的算法。当数据集针对查询包含许多冗余或非常相似的点时,它表现尤为出色。
MMR 迭代选择候选者,从最相关的点(与查询相似度最高)开始。对于接下来的每一个点,它会选择尚未被选中且在相关性与已选点之间的分离度方面具有最佳组合的点。
$$ MMR = \arg \max_{D_i \in R\setminus S}[\lambda sim(D_i, Q) - (1 - \lambda)\max_{D_j \in S}sim(D_i, D_j)] $$
这在 Qdrant 中实现为最近邻查询的一个参数。您定义用于获取最近候选者的向量,以及一个 diversity(多样性)参数,用于控制相关性 (0.0) 和多样性 (1.0) 之间的平衡。
POST /collections/{collection_name}/points/query
{
"query": {
"nearest": [0.01, 0.45, 0.67, ...], // search vector
"mmr": {
"diversity": 0.5, // 0.0 - relevance; 1.0 - diversity
"candidates_limit": 100 // num of candidates to preselect
}
},
"limit": 10
}
from qdrant_client import QdrantClient, models
client = QdrantClient(url="https://:6333")
client.query_points(
collection_name="{collection_name}",
query=models.NearestQuery(
nearest=[0.01, 0.45, 0.67], # search vector
mmr=models.Mmr(
diversity=0.5, # 0.0 - relevance; 1.0 - diversity
candidates_limit=100, # num of candidates to preselect
)
),
limit=10,
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ host: "localhost", port: 6333 });
client.query("{collection_name}", {
query: {
nearest: [0.01, 0.45, 0.67], // search vector
mmr: {
diversity: 0.5, // 0.0 - relevance; 1.0 - diversity
candidates_limit: 100 // num of candidates to preselect
}
},
limit: 10,
});
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{MmrBuilder, Query, QueryPointsBuilder};
let client = Qdrant::from_url("https://:6334").build()?;
client.query(
QueryPointsBuilder::new("{collection_name}")
.query(Query::new_nearest_with_mmr(
vec![0.01, 0.45, 0.67], // search vector
MmrBuilder::new()
.diversity(0.5) // 0.0 - relevance; 1.0 - diversity
.candidates_limit(100) // num of candidates to preselect
))
.limit(10)
).await?;
import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.VectorInputFactory.vectorInput;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.Mmr;
import io.qdrant.client.grpc.Points.QueryPoints;
QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client
.queryAsync(
QueryPoints.newBuilder()
.setCollectionName("{collection_name}")
.setQuery(
nearest(
vectorInput(0.01f, 0.45f, 0.67f), // <-- search vector
Mmr.newBuilder()
.setDiversity(0.5f) // 0.0 - relevance; 1.0 - diversity
.setCandidatesLimit(100) // num of candidates to preselect
.build()))
.setLimit(10)
.build())
.get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
var client = new QdrantClient("localhost", 6334);
await client.QueryAsync(
collectionName: "{collection_name}",
query: (
new float[] { 0.01f, 0.45f, 0.67f },
new Mmr
{
Diversity = 0.5f, // 0.0 - relevance; 1.0 - diversity
CandidatesLimit = 100 // Number of candidates to preselect
}
),
limit: 10
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Query(context.Background(), &qdrant.QueryPoints{
CollectionName: "{collection_name}",
Query: qdrant.NewQueryMMR(
qdrant.NewVectorInput(0.01, 0.45, 0.67),
&qdrant.Mmr{
Diversity: qdrant.PtrOf(float32(0.5)), // 0.0 - relevance; 1.0 - diversity
CandidatesLimit: qdrant.PtrOf(uint32(100)), // num of candidates to preselect
}),
Limit: qdrant.PtrOf(uint64(10)),
})
注意:由于 MMR 每次排名一个点,Qdrant 中 MMR 产生的分数是指与查询向量的相似度。这意味着响应不会按分数排序,而是按 MMR 的选择顺序排列。
相关性反馈
自 1.17 版本起可用
相关性反馈 (Relevance feedback) 将当前搜索结果中的信号提取到下一次检索迭代中,以浮现更相关的文档。
Qdrant 提供了一种基于相关性反馈的检索子类型,其中反馈由任何模型(相关性预言机)以细粒度方式提供:它根据检索结果与查询的相对相关性对其进行重评分。有关相关性反馈方法的详细概述,请参阅信息检索中的相关性反馈。
要使用基于相关性反馈的检索,需要两个组件:
- 一个用于搜索的向量集合。
- 一个用于确定搜索结果相关性的预言机 (Oracle)。
基于相关性反馈的检索背后的思路如下:
- 运行基本的最近邻搜索。我们称其结果为 检索器相似度 (Retriever Similarity),而搜索背后的算法称为 检索器 (retriever)。
- 使用任何 反馈模型 为前 X 个搜索结果分配相关性分数(X 不需要很大,3-5 是一个不错的选择)。我们将这些分数称为 反馈分数 (Feedback Score)。
- 通过分析前几个结果的反馈分数,确定反馈模型是否与检索器一致,或者检索是否可以改进。
- 如果可以改进,则使用反馈来修改检索(向量空间遍历),以解决反馈模型和检索器之间的差异。
例如,在这一组检索结果中:
| 点 ID | 检索器相似度 | 反馈分数 |
|---|---|---|
| 111 | 0.89 | 0.68 |
| 222 | 0.81 | 0.72 |
| 333 | 0.77 | 0.61 |
反馈模型认为 ID 为 222 的第二个结果最相关,这与检索器的排名存在差异。因此,此反馈可能有助于使下一次检索迭代更好。
为了在整个集合的搜索中利用反馈,Qdrant 提供了一个查询接口,该接口需要:
- 原始查询 (
target),可以是点 ID、推理对象或原始向量。 - 初始检索结果及其相关性分数 (
feedback) 的简短列表。每个反馈项包含:example,可以是点 ID、推理对象或检索器使用的原始向量。score,即反馈分数。
- 定义基于反馈修改检索的公式 (
strategy)。
POST /collections/{collection_name}/points/query
{
"query": {
"relevance_feedback": {
"target": [0.1, 0.9, 0.23, ...],
"feedback": [
{ "example": 111, "score": 0.68 },
{ "example": 222, "score": 0.72 },
{ "example": 333, "score": 0.61 }
],
"strategy": {
"naive": {
"a": 0.12,
"b": 0.43,
"c": 0.03
}
}
}
}
}
from qdrant_client import QdrantClient, models
client = QdrantClient()
client.query_points(
"{collection_name}",
query=models.RelevanceFeedbackQuery(
relevance_feedback=models.RelevanceFeedbackInput(
target=[0.1, 0.9, 0.23],
feedback=[
models.FeedbackItem(example=111, score=0.68),
models.FeedbackItem(example=222, score=0.72),
models.FeedbackItem(example=333, score=0.61),
],
strategy=models.NaiveFeedbackStrategy(
naive=models.NaiveFeedbackStrategyParams(
a=0.12,
b=0.43,
c=0.03
)
)
)
)
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ host: "localhost", port: 6333 });
client.query("{collection_name}", {
query: {
relevance_feedback: {
target: [0.1, 0.9, 0.23],
feedback: [
{ example: 111, score: 0.68 },
{ example: 222, score: 0.72 },
{ example: 333, score: 0.61 },
],
strategy: {
naive: {
a: 0.12,
b: 0.43,
c: 0.03,
},
},
},
},
});
use qdrant_client::qdrant::{
FeedbackItemBuilder, FeedbackStrategyBuilder, PointId, Query, QueryPointsBuilder,
RelevanceFeedbackInputBuilder, VectorInput,
};
use qdrant_client::Qdrant;
let _points = client.query(
QueryPointsBuilder::new("{collection_name}")
.query(Query::new_relevance_feedback(
RelevanceFeedbackInputBuilder::new(vec![0.01, 0.45, 0.67])
.add_feedback(FeedbackItemBuilder::new(VectorInput::new_id(PointId::from(111)), 0.68))
.add_feedback(FeedbackItemBuilder::new(VectorInput::new_id(PointId::from(222)), 0.72))
.add_feedback(FeedbackItemBuilder::new(VectorInput::new_id(PointId::from(333)), 0.61))
.strategy(FeedbackStrategyBuilder::naive(0.12, 0.43, 0.16))
))
.limit(10u64)
).await?;
import static io.qdrant.client.QueryFactory.relevanceFeedback;
import static io.qdrant.client.VectorInputFactory.vectorInput;
import io.qdrant.client.grpc.Points.FeedbackItem;
import io.qdrant.client.grpc.Points.FeedbackStrategy;
import io.qdrant.client.grpc.Points.NaiveFeedbackStrategy;
import io.qdrant.client.grpc.Points.QueryPoints;
import io.qdrant.client.grpc.Points.RelevanceFeedbackInput;
import java.util.List;
client
.queryAsync(
QueryPoints.newBuilder()
.setCollectionName("{collection_name}")
.setQuery(
relevanceFeedback(
RelevanceFeedbackInput.newBuilder()
.setTarget(vectorInput(0.01f, 0.45f, 0.67f))
.addFeedback(
FeedbackItem.newBuilder()
.setExample(vectorInput(111))
.setScore(0.68f)
.build())
.addFeedback(
FeedbackItem.newBuilder()
.setExample(vectorInput(222))
.setScore(0.72f)
.build())
.addFeedback(
FeedbackItem.newBuilder()
.setExample(vectorInput(333))
.setScore(0.61f)
.build())
.setStrategy(
FeedbackStrategy.newBuilder()
.setNaive(
NaiveFeedbackStrategy.newBuilder()
.setA(0.12f)
.setB(0.43f)
.setC(0.16f)
.build())
.build())
.build()))
.build())
.get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
await client.QueryAsync(
collectionName: "{collection_name}",
query: new RelevanceFeedbackInput
{
Target = new float[] { 0.2f, 0.1f, 0.9f, 0.7f },
Feedback =
{
new FeedbackItem { Example = 111, Score = 0.68f },
new FeedbackItem { Example = 222, Score = 0.72f },
new FeedbackItem { Example = 33, Score = 0.61f },
},
Strategy =
{
Naive = new()
{
A = 0.12f,
B = 0.43f,
C = 0.16f,
},
},
}
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client.Query(context.Background(), &qdrant.QueryPoints{
CollectionName: "{collection_name}",
Query: qdrant.NewQueryRelevanceFeedback(
&qdrant.RelevanceFeedbackInput{
Target: qdrant.NewVectorInput(0.01, 0.45, 0.67),
Feedback: []*qdrant.FeedbackItem{
{
Example: qdrant.NewVectorInputID(qdrant.NewIDNum(111)),
Score: 0.68,
},
{
Example: qdrant.NewVectorInputID(qdrant.NewIDNum(222)),
Score: 0.72,
},
{
Example: qdrant.NewVectorInputID(qdrant.NewIDNum(333)),
Score: 0.61,
},
},
Strategy: qdrant.NewFeedbackStrategyNaive(&qdrant.NaiveFeedbackStrategy{
A: 0.12, B: 0.43, C: 0.16,
}),
},
),
})
在内部,Qdrant 根据相关性分数将反馈列表组合成对,然后在修改检索期间向量空间遍历的公式中使用这些对(更改检索策略)。这种基于相关性反馈的检索不仅考虑了候选者与查询的相似度,还考虑了候选者与每个反馈对的相似度。有关其工作原理的更详细描述,请参阅文章Qdrant 中的相关性反馈。
naive 策略的 a、b 和 c 参数需要针对检索器、反馈模型和集合的每个三元组进行定制。为了使这 3 个权重适应您的设置,请使用我们的开源 Python 包。
有关确定参数、将其与相关性反馈查询一起使用以及评估结果的动手教程,请查看Qdrant 中的相关性反馈。
朴素策略 (Naive Strategy)
目前,naive 是唯一可用的策略。
朴素策略 (Naive Strategy)
$$ score = a * sim(query, candidate) + \sum_{pair \in pairs}{(confidence_{pair})^b * c * delta_{pair}} \\ $$ \begin{align} \text{where} \\ confidence_{pair} &= relevance_{positive} - relevance_{negative} \\ delta_{pair} &= sim(positive, candidate) - sim(negative, candidate) \\ \end{align}