mirror of
https://github.com/qdrant/landing_page.git
synced 2026-10-04 02:18:29 +02:00
Merge pull request #2357 from qdrant/hybrid-search-gap-3
Document fusion methods: weighted RRF, DBSF, FormulaQuery
This commit is contained in:
@@ -156,7 +156,7 @@ For custom fusion, use the [Formula Query](/documentation/search/search-relevanc
|
||||
|
||||
To evaluate which works better for your use case, create a small golden query set and compare [retrieval quality metrics](/documentation/improve-search/retrieval-relevance/) (for example, NDCG@10) under each method.
|
||||
|
||||
See also: [Hybrid Queries](/documentation/search/hybrid-queries/)
|
||||
See also: the [Choosing a Fusion Method](/documentation/search/hybrid-queries/#choosing-a-fusion-method) decision table in the Hybrid Queries reference, and the [Choosing a Fusion Method notebook](https://github.com/qdrant/examples/blob/master/fusion-methods/Choosing_a_Fusion_Method.ipynb) for a runnable RRF vs weighted RRF vs DBSF eval on BEIR/SciFact with a reusable weight-tuning helper.
|
||||
|
||||
### My hybrid search results aren't relevant. Where do I start debugging?
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
This code snippet runs a hybrid query that fuses sparse and dense results with Distribution-Based Score Fusion (DBSF). DBSF normalizes each retriever's score distribution using the mean and three standard deviations as limits, then sums the normalized scores. Use it when the raw scores carry magnitude information you want to preserve, rather than discarding score information as RRF does.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using Qdrant.Client;
|
||||
using Qdrant.Client.Grpc;
|
||||
|
||||
public class Snippet
|
||||
{
|
||||
public static async Task Run()
|
||||
{
|
||||
var client = new QdrantClient("localhost", 6334);
|
||||
|
||||
await client.QueryAsync(
|
||||
collectionName: "{collection_name}",
|
||||
prefetch: new List < PrefetchQuery > {
|
||||
new() {
|
||||
Query = new(float, uint)[] {
|
||||
(0.22f, 1), (0.8f, 42),
|
||||
},
|
||||
Using = "sparse",
|
||||
Limit = 20
|
||||
},
|
||||
new() {
|
||||
Query = new float[] {
|
||||
0.01f, 0.45f, 0.67f
|
||||
},
|
||||
Using = "dense",
|
||||
Limit = 20
|
||||
}
|
||||
},
|
||||
query: Fusion.Dbsf
|
||||
);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
```csharp
|
||||
using Qdrant.Client;
|
||||
using Qdrant.Client.Grpc;
|
||||
|
||||
var client = new QdrantClient("localhost", 6334);
|
||||
|
||||
await client.QueryAsync(
|
||||
collectionName: "{collection_name}",
|
||||
prefetch: new List < PrefetchQuery > {
|
||||
new() {
|
||||
Query = new(float, uint)[] {
|
||||
(0.22f, 1), (0.8f, 42),
|
||||
},
|
||||
Using = "sparse",
|
||||
Limit = 20
|
||||
},
|
||||
new() {
|
||||
Query = new float[] {
|
||||
0.01f, 0.45f, 0.67f
|
||||
},
|
||||
Using = "dense",
|
||||
Limit = 20
|
||||
}
|
||||
},
|
||||
query: Fusion.Dbsf
|
||||
);
|
||||
```
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
```go
|
||||
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.NewQuerySparse([]uint32{1, 42}, []float32{0.22, 0.8}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDense([]float32{0.01, 0.45, 0.67}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFusion(qdrant.Fusion_DBSF),
|
||||
})
|
||||
```
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
```java
|
||||
import static io.qdrant.client.QueryFactory.fusion;
|
||||
import static io.qdrant.client.QueryFactory.nearest;
|
||||
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.QdrantGrpcClient;
|
||||
import io.qdrant.client.grpc.Points.Fusion;
|
||||
import io.qdrant.client.grpc.Points.PrefetchQuery;
|
||||
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||
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(List.of(0.22f, 0.8f), List.of(1, 42)))
|
||||
.setUsing("sparse")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.addPrefetch(PrefetchQuery.newBuilder()
|
||||
.setQuery(nearest(List.of(0.01f, 0.45f, 0.67f)))
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(fusion(Fusion.DBSF))
|
||||
.build())
|
||||
.get();
|
||||
```
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
```python
|
||||
from qdrant_client import QdrantClient, models
|
||||
|
||||
client = QdrantClient(url="http://localhost:6333")
|
||||
|
||||
client.query_points(
|
||||
collection_name="{collection_name}",
|
||||
prefetch=[
|
||||
models.Prefetch(
|
||||
query=models.SparseVector(indices=[1, 42], values=[0.22, 0.8]),
|
||||
using="sparse",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=[0.01, 0.45, 0.67], # <-- dense vector
|
||||
using="dense",
|
||||
limit=20,
|
||||
),
|
||||
],
|
||||
query=models.FusionQuery(fusion=models.Fusion.DBSF),
|
||||
)
|
||||
```
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
```rust
|
||||
use qdrant_client::Qdrant;
|
||||
use qdrant_client::qdrant::{Fusion, PrefetchQueryBuilder, Query, QueryPointsBuilder};
|
||||
|
||||
let client = Qdrant::from_url("http://localhost:6334").build()?;
|
||||
|
||||
client.query(
|
||||
QueryPointsBuilder::new("{collection_name}")
|
||||
.add_prefetch(PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest([(1, 0.22), (42, 0.8)].as_slice()))
|
||||
.using("sparse")
|
||||
.limit(20u64)
|
||||
)
|
||||
.add_prefetch(PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(vec![0.01, 0.45, 0.67]))
|
||||
.using("dense")
|
||||
.limit(20u64)
|
||||
)
|
||||
.query(Query::new_fusion(Fusion::Dbsf))
|
||||
).await?;
|
||||
```
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
```typescript
|
||||
import { QdrantClient } from "@qdrant/js-client-rest";
|
||||
|
||||
const client = new QdrantClient({ host: "localhost", port: 6333 });
|
||||
|
||||
client.query("{collection_name}", {
|
||||
prefetch: [
|
||||
{
|
||||
query: {
|
||||
values: [0.22, 0.8],
|
||||
indices: [1, 42],
|
||||
},
|
||||
using: 'sparse',
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
query: [0.01, 0.45, 0.67],
|
||||
using: 'dense',
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
query: {
|
||||
fusion: 'dbsf',
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
package snippet
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdrant/go-client/qdrant"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
client, err := qdrant.NewClient(&qdrant.Config{
|
||||
Host: "localhost",
|
||||
Port: 6334,
|
||||
})
|
||||
|
||||
if err != nil { panic(err) } // @hide
|
||||
|
||||
client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: "{collection_name}",
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQuerySparse([]uint32{1, 42}, []float32{0.22, 0.8}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDense([]float32{0.01, 0.45, 0.67}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFusion(qdrant.Fusion_DBSF),
|
||||
})
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
```http
|
||||
POST /collections/{collection_name}/points/query
|
||||
{
|
||||
"prefetch": [
|
||||
{
|
||||
"query": {
|
||||
"indices": [1, 42], // <┐
|
||||
"values": [0.22, 0.8] // <┴─sparse vector
|
||||
},
|
||||
"using": "sparse",
|
||||
"limit": 20
|
||||
},
|
||||
{
|
||||
"query": [0.01, 0.45, 0.67, ...], // <-- dense vector
|
||||
"using": "dense",
|
||||
"limit": 20
|
||||
}
|
||||
],
|
||||
"query": { "fusion": "dbsf" }, // <--- distribution-based score fusion
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.example.snippets_amalgamation;
|
||||
|
||||
import static io.qdrant.client.QueryFactory.fusion;
|
||||
import static io.qdrant.client.QueryFactory.nearest;
|
||||
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.QdrantGrpcClient;
|
||||
import io.qdrant.client.grpc.Points.Fusion;
|
||||
import io.qdrant.client.grpc.Points.PrefetchQuery;
|
||||
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||
import java.util.List;
|
||||
|
||||
public class Snippet {
|
||||
public static void run() throws Exception {
|
||||
QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
|
||||
|
||||
client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName("{collection_name}")
|
||||
.addPrefetch(PrefetchQuery.newBuilder()
|
||||
.setQuery(nearest(List.of(0.22f, 0.8f), List.of(1, 42)))
|
||||
.setUsing("sparse")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.addPrefetch(PrefetchQuery.newBuilder()
|
||||
.setQuery(nearest(List.of(0.01f, 0.45f, 0.67f)))
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(fusion(Fusion.DBSF))
|
||||
.build())
|
||||
.get();
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from qdrant_client import QdrantClient, models
|
||||
|
||||
client = QdrantClient(url="http://localhost:6333")
|
||||
|
||||
client.query_points(
|
||||
collection_name="{collection_name}",
|
||||
prefetch=[
|
||||
models.Prefetch(
|
||||
query=models.SparseVector(indices=[1, 42], values=[0.22, 0.8]),
|
||||
using="sparse",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=[0.01, 0.45, 0.67], # <-- dense vector
|
||||
using="dense",
|
||||
limit=20,
|
||||
),
|
||||
],
|
||||
query=models.FusionQuery(fusion=models.Fusion.DBSF),
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
use qdrant_client::Qdrant;
|
||||
use qdrant_client::qdrant::{Fusion, PrefetchQueryBuilder, Query, QueryPointsBuilder};
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let client = Qdrant::from_url("http://localhost:6334").build()?;
|
||||
|
||||
client.query(
|
||||
QueryPointsBuilder::new("{collection_name}")
|
||||
.add_prefetch(PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest([(1, 0.22), (42, 0.8)].as_slice()))
|
||||
.using("sparse")
|
||||
.limit(20u64)
|
||||
)
|
||||
.add_prefetch(PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(vec![0.01, 0.45, 0.67]))
|
||||
.using("dense")
|
||||
.limit(20u64)
|
||||
)
|
||||
.query(Query::new_fusion(Fusion::Dbsf))
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { QdrantClient } from "@qdrant/js-client-rest";
|
||||
|
||||
const client = new QdrantClient({ host: "localhost", port: 6333 });
|
||||
|
||||
client.query("{collection_name}", {
|
||||
prefetch: [
|
||||
{
|
||||
query: {
|
||||
values: [0.22, 0.8],
|
||||
indices: [1, 42],
|
||||
},
|
||||
using: 'sparse',
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
query: [0.01, 0.45, 0.67],
|
||||
using: 'dense',
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
query: {
|
||||
fusion: 'dbsf',
|
||||
},
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
This code snippet shows the canonical pattern for combining hybrid search fusion with business-logic ranking. The inner prefetch fuses sparse and dense results with RRF, then the outer `FormulaQuery` applies exponential decay on a `published_at` payload field to boost recent documents. The decay term is wrapped in `mult` with a `0.1` coefficient so it nudges the ranking rather than crowding out the small RRF scores. Use this pattern any time you want fusion plus recency, popularity, geo decay, or category-conditional multipliers, rather than trying to encode those signals as fusion weights.
|
||||
|
||||
For production: `published_at` must exist on every point, or supply `defaults` in the `FormulaQuery` to fill missing values. A datetime payload index on `published_at` keeps query latency low.
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
using Qdrant.Client;
|
||||
using Qdrant.Client.Grpc;
|
||||
|
||||
public class Snippet
|
||||
{
|
||||
public static async Task Run()
|
||||
{
|
||||
var client = new QdrantClient("localhost", 6334);
|
||||
|
||||
await client.QueryAsync(
|
||||
collectionName: "{collection_name}",
|
||||
prefetch:
|
||||
[
|
||||
new PrefetchQuery {
|
||||
Prefetch = {
|
||||
new PrefetchQuery {
|
||||
Query = new(float, uint)[] { (0.22f, 1), (0.8f, 42) },
|
||||
Using = "sparse",
|
||||
Limit = 100
|
||||
},
|
||||
new PrefetchQuery {
|
||||
Query = new float[] { 0.01f, 0.45f, 0.67f },
|
||||
Using = "dense",
|
||||
Limit = 100
|
||||
},
|
||||
},
|
||||
Query = new Rrf(),
|
||||
Limit = 100
|
||||
},
|
||||
],
|
||||
query: new Formula
|
||||
{
|
||||
Expression = new SumExpression
|
||||
{
|
||||
Sum =
|
||||
{
|
||||
"$score", // the fused score from the RRF prefetch
|
||||
new MultExpression
|
||||
{
|
||||
Mult =
|
||||
{
|
||||
0.1f, // caps decay contribution
|
||||
Expression.FromExpDecay(
|
||||
new()
|
||||
{
|
||||
X = Expression.FromDateTimeKey("published_at"),
|
||||
Target = Expression.FromDateTime("YYYY-MM-DDT00:00:00Z"),
|
||||
Scale = 86400 * 180, // 180 days in seconds
|
||||
Midpoint = 0.5f
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
limit: 10
|
||||
);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
```csharp
|
||||
using Qdrant.Client;
|
||||
using Qdrant.Client.Grpc;
|
||||
|
||||
var client = new QdrantClient("localhost", 6334);
|
||||
|
||||
await client.QueryAsync(
|
||||
collectionName: "{collection_name}",
|
||||
prefetch:
|
||||
[
|
||||
new PrefetchQuery {
|
||||
Prefetch = {
|
||||
new PrefetchQuery {
|
||||
Query = new(float, uint)[] { (0.22f, 1), (0.8f, 42) },
|
||||
Using = "sparse",
|
||||
Limit = 100
|
||||
},
|
||||
new PrefetchQuery {
|
||||
Query = new float[] { 0.01f, 0.45f, 0.67f },
|
||||
Using = "dense",
|
||||
Limit = 100
|
||||
},
|
||||
},
|
||||
Query = new Rrf(),
|
||||
Limit = 100
|
||||
},
|
||||
],
|
||||
query: new Formula
|
||||
{
|
||||
Expression = new SumExpression
|
||||
{
|
||||
Sum =
|
||||
{
|
||||
"$score", // the fused score from the RRF prefetch
|
||||
new MultExpression
|
||||
{
|
||||
Mult =
|
||||
{
|
||||
0.1f, // caps decay contribution
|
||||
Expression.FromExpDecay(
|
||||
new()
|
||||
{
|
||||
X = Expression.FromDateTimeKey("published_at"),
|
||||
Target = Expression.FromDateTime("YYYY-MM-DDT00:00:00Z"),
|
||||
Scale = 86400 * 180, // 180 days in seconds
|
||||
Midpoint = 0.5f
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
limit: 10
|
||||
);
|
||||
```
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
```go
|
||||
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{
|
||||
{
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQuerySparse([]uint32{1, 42}, []float32{0.22, 0.8}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(100)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDense([]float32{0.01, 0.45, 0.67}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(100)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryRRF(&qdrant.Rrf{}),
|
||||
Limit: qdrant.PtrOf(uint64(100)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFormula(&qdrant.Formula{
|
||||
Expression: qdrant.NewExpressionSum(&qdrant.SumExpression{
|
||||
Sum: []*qdrant.Expression{
|
||||
qdrant.NewExpressionVariable("$score"), // the fused score from the RRF prefetch
|
||||
qdrant.NewExpressionMult(&qdrant.MultExpression{
|
||||
Mult: []*qdrant.Expression{
|
||||
qdrant.NewExpressionConstant(0.1), // caps decay contribution
|
||||
qdrant.NewExpressionExpDecay(&qdrant.DecayParamsExpression{
|
||||
X: qdrant.NewExpressionDatetimeKey("published_at"),
|
||||
Target: qdrant.NewExpressionDatetime("YYYY-MM-DDT00:00:00Z"),
|
||||
Scale: qdrant.PtrOf(float32(86400 * 180)), // 180 days in seconds
|
||||
Midpoint: qdrant.PtrOf(float32(0.5)),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
```
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
```java
|
||||
import static io.qdrant.client.ExpressionFactory.constant;
|
||||
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.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 static io.qdrant.client.QueryFactory.rrf;
|
||||
|
||||
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.MultExpression;
|
||||
import io.qdrant.client.grpc.Points.PrefetchQuery;
|
||||
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||
import io.qdrant.client.grpc.Points.Rrf;
|
||||
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()
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(nearest(List.of(0.22f, 0.8f), List.of(1, 42)))
|
||||
.setUsing("sparse")
|
||||
.setLimit(100)
|
||||
.build())
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(nearest(List.of(0.01f, 0.45f, 0.67f)))
|
||||
.setUsing("dense")
|
||||
.setLimit(100)
|
||||
.build())
|
||||
.setQuery(rrf(Rrf.newBuilder().build()))
|
||||
.setLimit(100)
|
||||
.build())
|
||||
.setQuery(
|
||||
formula(
|
||||
Formula.newBuilder()
|
||||
.setExpression(
|
||||
sum(
|
||||
SumExpression.newBuilder()
|
||||
.addSum(variable("$score"))
|
||||
.addSum(
|
||||
mult(
|
||||
MultExpression.newBuilder()
|
||||
.addMult(constant(0.1f))
|
||||
.addMult(
|
||||
expDecay(
|
||||
DecayParamsExpression.newBuilder()
|
||||
.setX(datetimeKey("published_at"))
|
||||
.setTarget(
|
||||
datetime("YYYY-MM-DDT00:00:00Z"))
|
||||
.setScale(86400 * 180)
|
||||
.setMidpoint(0.5f)
|
||||
.build()))
|
||||
.build()))
|
||||
.build()))
|
||||
.build()))
|
||||
.setLimit(10)
|
||||
.build())
|
||||
.get();
|
||||
```
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
```python
|
||||
from qdrant_client import QdrantClient, models
|
||||
|
||||
client = QdrantClient(url="http://localhost:6333")
|
||||
|
||||
client.query_points(
|
||||
collection_name="{collection_name}",
|
||||
prefetch=models.Prefetch(
|
||||
prefetch=[
|
||||
models.Prefetch(
|
||||
query=models.SparseVector(indices=[1, 42], values=[0.22, 0.8]),
|
||||
using="sparse",
|
||||
limit=100,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=[0.01, 0.45, 0.67], # <-- dense vector
|
||||
using="dense",
|
||||
limit=100,
|
||||
),
|
||||
],
|
||||
query=models.RrfQuery(rrf=models.Rrf()),
|
||||
limit=100,
|
||||
),
|
||||
query=models.FormulaQuery(
|
||||
formula=models.SumExpression(
|
||||
sum=[
|
||||
"$score", # the fused score from the RRF prefetch
|
||||
models.MultExpression(mult=[
|
||||
0.1, # caps decay contribution; un-weighted decay [0, 1] would otherwise crowd out small RRF scores
|
||||
models.ExpDecayExpression(
|
||||
exp_decay=models.DecayParamsExpression(
|
||||
x=models.DatetimeKeyExpression(datetime_key="published_at"),
|
||||
target=models.DatetimeExpression(datetime="YYYY-MM-DDT00:00:00Z"),
|
||||
scale=86400 * 180, # 180 days in seconds
|
||||
midpoint=0.5,
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
)
|
||||
),
|
||||
limit=10,
|
||||
)
|
||||
```
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
```rust
|
||||
use qdrant_client::Qdrant;
|
||||
use qdrant_client::qdrant::{
|
||||
DecayParamsExpressionBuilder, Expression, FormulaBuilder, PrefetchQueryBuilder, Query,
|
||||
QueryPointsBuilder, RrfBuilder,
|
||||
};
|
||||
|
||||
let client = Qdrant::from_url("http://localhost:6334").build()?;
|
||||
|
||||
client.query(
|
||||
QueryPointsBuilder::new("{collection_name}")
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest([(1, 0.22), (42, 0.8)].as_slice()))
|
||||
.using("sparse")
|
||||
.limit(100u64),
|
||||
)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(vec![0.01, 0.45, 0.67]))
|
||||
.using("dense")
|
||||
.limit(100u64),
|
||||
)
|
||||
.query(Query::new_rrf(RrfBuilder::default()))
|
||||
.limit(100u64),
|
||||
)
|
||||
.query(
|
||||
FormulaBuilder::new(Expression::sum_with([
|
||||
Expression::score(),
|
||||
Expression::mult_with([
|
||||
Expression::constant(0.1),
|
||||
Expression::exp_decay(
|
||||
DecayParamsExpressionBuilder::new(Expression::datetime_key("published_at"))
|
||||
.target(Expression::datetime("YYYY-MM-DDT00:00:00Z"))
|
||||
.scale(86400.0 * 180.0)
|
||||
.midpoint(0.5),
|
||||
),
|
||||
]),
|
||||
])),
|
||||
)
|
||||
.limit(10u64),
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
```typescript
|
||||
import { QdrantClient } from "@qdrant/js-client-rest";
|
||||
|
||||
const client = new QdrantClient({ host: "localhost", port: 6333 });
|
||||
|
||||
await client.query("{collection_name}", {
|
||||
prefetch: {
|
||||
prefetch: [
|
||||
{
|
||||
query: {
|
||||
values: [0.22, 0.8],
|
||||
indices: [1, 42],
|
||||
},
|
||||
using: "sparse",
|
||||
limit: 100,
|
||||
},
|
||||
{
|
||||
query: [0.01, 0.45, 0.67], // <-- dense vector
|
||||
using: "dense",
|
||||
limit: 100,
|
||||
},
|
||||
],
|
||||
query: { rrf: {} },
|
||||
limit: 100,
|
||||
},
|
||||
query: {
|
||||
formula: {
|
||||
sum: [
|
||||
"$score", // the fused score from the RRF prefetch
|
||||
{
|
||||
mult: [
|
||||
0.1, // caps decay contribution; un-weighted decay [0, 1] would otherwise crowd out small RRF scores
|
||||
{
|
||||
exp_decay: {
|
||||
x: { datetime_key: "published_at" },
|
||||
target: { datetime: "YYYY-MM-DDT00:00:00Z" },
|
||||
scale: 86400 * 180, // 180 days in seconds
|
||||
midpoint: 0.5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
limit: 10,
|
||||
});
|
||||
```
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package snippet
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdrant/go-client/qdrant"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
client, err := qdrant.NewClient(&qdrant.Config{
|
||||
Host: "localhost",
|
||||
Port: 6334,
|
||||
})
|
||||
|
||||
if err != nil { panic(err) } // @hide
|
||||
|
||||
client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: "{collection_name}",
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQuerySparse([]uint32{1, 42}, []float32{0.22, 0.8}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(100)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDense([]float32{0.01, 0.45, 0.67}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(100)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryRRF(&qdrant.Rrf{}),
|
||||
Limit: qdrant.PtrOf(uint64(100)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFormula(&qdrant.Formula{
|
||||
Expression: qdrant.NewExpressionSum(&qdrant.SumExpression{
|
||||
Sum: []*qdrant.Expression{
|
||||
qdrant.NewExpressionVariable("$score"), // the fused score from the RRF prefetch
|
||||
qdrant.NewExpressionMult(&qdrant.MultExpression{
|
||||
Mult: []*qdrant.Expression{
|
||||
qdrant.NewExpressionConstant(0.1), // caps decay contribution
|
||||
qdrant.NewExpressionExpDecay(&qdrant.DecayParamsExpression{
|
||||
X: qdrant.NewExpressionDatetimeKey("published_at"),
|
||||
Target: qdrant.NewExpressionDatetime("YYYY-MM-DDT00:00:00Z"),
|
||||
Scale: qdrant.PtrOf(float32(86400 * 180)), // 180 days in seconds
|
||||
Midpoint: qdrant.PtrOf(float32(0.5)),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
```http
|
||||
POST /collections/{collection_name}/points/query
|
||||
{
|
||||
"prefetch": {
|
||||
"prefetch": [
|
||||
{
|
||||
"query": {
|
||||
"indices": [1, 42], // <┐
|
||||
"values": [0.22, 0.8] // <┴─sparse vector
|
||||
},
|
||||
"using": "sparse",
|
||||
"limit": 100
|
||||
},
|
||||
{
|
||||
"query": [0.01, 0.45, 0.67, ...], // <-- dense vector
|
||||
"using": "dense",
|
||||
"limit": 100
|
||||
}
|
||||
],
|
||||
"query": { "rrf": {} },
|
||||
"limit": 100
|
||||
},
|
||||
"query": {
|
||||
"formula": {
|
||||
"sum": [
|
||||
"$score", // the fused score from the RRF prefetch
|
||||
{
|
||||
"mult": [
|
||||
0.1, // caps decay contribution
|
||||
{
|
||||
"exp_decay": {
|
||||
"x": {
|
||||
"datetime_key": "published_at"
|
||||
},
|
||||
"target": {
|
||||
"datetime": "YYYY-MM-DDT00:00:00Z"
|
||||
},
|
||||
"scale": 15552000, // 180 days in seconds
|
||||
"midpoint": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.example.snippets_amalgamation;
|
||||
|
||||
import static io.qdrant.client.ExpressionFactory.constant;
|
||||
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.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 static io.qdrant.client.QueryFactory.rrf;
|
||||
|
||||
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.MultExpression;
|
||||
import io.qdrant.client.grpc.Points.PrefetchQuery;
|
||||
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||
import io.qdrant.client.grpc.Points.Rrf;
|
||||
import io.qdrant.client.grpc.Points.SumExpression;
|
||||
import java.util.List;
|
||||
|
||||
public class Snippet {
|
||||
public static void run() throws Exception {
|
||||
QdrantClient client =
|
||||
new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
|
||||
|
||||
client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName("{collection_name}")
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(nearest(List.of(0.22f, 0.8f), List.of(1, 42)))
|
||||
.setUsing("sparse")
|
||||
.setLimit(100)
|
||||
.build())
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(nearest(List.of(0.01f, 0.45f, 0.67f)))
|
||||
.setUsing("dense")
|
||||
.setLimit(100)
|
||||
.build())
|
||||
.setQuery(rrf(Rrf.newBuilder().build()))
|
||||
.setLimit(100)
|
||||
.build())
|
||||
.setQuery(
|
||||
formula(
|
||||
Formula.newBuilder()
|
||||
.setExpression(
|
||||
sum(
|
||||
SumExpression.newBuilder()
|
||||
.addSum(variable("$score"))
|
||||
.addSum(
|
||||
mult(
|
||||
MultExpression.newBuilder()
|
||||
.addMult(constant(0.1f))
|
||||
.addMult(
|
||||
expDecay(
|
||||
DecayParamsExpression.newBuilder()
|
||||
.setX(datetimeKey("published_at"))
|
||||
.setTarget(
|
||||
datetime("YYYY-MM-DDT00:00:00Z"))
|
||||
.setScale(86400 * 180)
|
||||
.setMidpoint(0.5f)
|
||||
.build()))
|
||||
.build()))
|
||||
.build()))
|
||||
.build()))
|
||||
.setLimit(10)
|
||||
.build())
|
||||
.get();
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
from qdrant_client import QdrantClient, models
|
||||
|
||||
client = QdrantClient(url="http://localhost:6333")
|
||||
|
||||
client.query_points(
|
||||
collection_name="{collection_name}",
|
||||
prefetch=models.Prefetch(
|
||||
prefetch=[
|
||||
models.Prefetch(
|
||||
query=models.SparseVector(indices=[1, 42], values=[0.22, 0.8]),
|
||||
using="sparse",
|
||||
limit=100,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=[0.01, 0.45, 0.67], # <-- dense vector
|
||||
using="dense",
|
||||
limit=100,
|
||||
),
|
||||
],
|
||||
query=models.RrfQuery(rrf=models.Rrf()),
|
||||
limit=100,
|
||||
),
|
||||
query=models.FormulaQuery(
|
||||
formula=models.SumExpression(
|
||||
sum=[
|
||||
"$score", # the fused score from the RRF prefetch
|
||||
models.MultExpression(mult=[
|
||||
0.1, # caps decay contribution; un-weighted decay [0, 1] would otherwise crowd out small RRF scores
|
||||
models.ExpDecayExpression(
|
||||
exp_decay=models.DecayParamsExpression(
|
||||
x=models.DatetimeKeyExpression(datetime_key="published_at"),
|
||||
target=models.DatetimeExpression(datetime="YYYY-MM-DDT00:00:00Z"),
|
||||
scale=86400 * 180, # 180 days in seconds
|
||||
midpoint=0.5,
|
||||
)
|
||||
),
|
||||
]),
|
||||
]
|
||||
)
|
||||
),
|
||||
limit=10,
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
use qdrant_client::Qdrant;
|
||||
use qdrant_client::qdrant::{
|
||||
DecayParamsExpressionBuilder, Expression, FormulaBuilder, PrefetchQueryBuilder, Query,
|
||||
QueryPointsBuilder, RrfBuilder,
|
||||
};
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let client = Qdrant::from_url("http://localhost:6334").build()?;
|
||||
|
||||
client.query(
|
||||
QueryPointsBuilder::new("{collection_name}")
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest([(1, 0.22), (42, 0.8)].as_slice()))
|
||||
.using("sparse")
|
||||
.limit(100u64),
|
||||
)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(vec![0.01, 0.45, 0.67]))
|
||||
.using("dense")
|
||||
.limit(100u64),
|
||||
)
|
||||
.query(Query::new_rrf(RrfBuilder::default()))
|
||||
.limit(100u64),
|
||||
)
|
||||
.query(
|
||||
FormulaBuilder::new(Expression::sum_with([
|
||||
Expression::score(),
|
||||
Expression::mult_with([
|
||||
Expression::constant(0.1),
|
||||
Expression::exp_decay(
|
||||
DecayParamsExpressionBuilder::new(Expression::datetime_key("published_at"))
|
||||
.target(Expression::datetime("YYYY-MM-DDT00:00:00Z"))
|
||||
.scale(86400.0 * 180.0)
|
||||
.midpoint(0.5),
|
||||
),
|
||||
]),
|
||||
])),
|
||||
)
|
||||
.limit(10u64),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { QdrantClient } from "@qdrant/js-client-rest";
|
||||
|
||||
const client = new QdrantClient({ host: "localhost", port: 6333 });
|
||||
|
||||
await client.query("{collection_name}", {
|
||||
prefetch: {
|
||||
prefetch: [
|
||||
{
|
||||
query: {
|
||||
values: [0.22, 0.8],
|
||||
indices: [1, 42],
|
||||
},
|
||||
using: "sparse",
|
||||
limit: 100,
|
||||
},
|
||||
{
|
||||
query: [0.01, 0.45, 0.67], // <-- dense vector
|
||||
using: "dense",
|
||||
limit: 100,
|
||||
},
|
||||
],
|
||||
query: { rrf: {} },
|
||||
limit: 100,
|
||||
},
|
||||
query: {
|
||||
formula: {
|
||||
sum: [
|
||||
"$score", // the fused score from the RRF prefetch
|
||||
{
|
||||
mult: [
|
||||
0.1, // caps decay contribution; un-weighted decay [0, 1] would otherwise crowd out small RRF scores
|
||||
{
|
||||
exp_decay: {
|
||||
x: { datetime_key: "published_at" },
|
||||
target: { datetime: "YYYY-MM-DDT00:00:00Z" },
|
||||
scale: 86400 * 180, // 180 days in seconds
|
||||
midpoint: 0.5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
limit: 10,
|
||||
});
|
||||
+1
-1
@@ -25,7 +25,7 @@ public class Snippet
|
||||
Limit = 20
|
||||
}
|
||||
},
|
||||
query: Fusion.Rrf
|
||||
query: new Rrf()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,6 +22,6 @@ await client.QueryAsync(
|
||||
Limit = 20
|
||||
}
|
||||
},
|
||||
query: Fusion.Rrf
|
||||
query: new Rrf()
|
||||
);
|
||||
```
|
||||
|
||||
+3
-1
@@ -16,12 +16,14 @@ client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
{
|
||||
Query: qdrant.NewQuerySparse([]uint32{1, 42}, []float32{0.22, 0.8}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDense([]float32{0.01, 0.45, 0.67}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFusion(qdrant.Fusion_RRF),
|
||||
Query: qdrant.NewQueryRRF(&qdrant.Rrf{}),
|
||||
})
|
||||
```
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
```java
|
||||
import static io.qdrant.client.QueryFactory.fusion;
|
||||
import static io.qdrant.client.QueryFactory.nearest;
|
||||
import static io.qdrant.client.QueryFactory.rrf;
|
||||
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.QdrantGrpcClient;
|
||||
import io.qdrant.client.grpc.Points.Fusion;
|
||||
import io.qdrant.client.grpc.Points.PrefetchQuery;
|
||||
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||
import io.qdrant.client.grpc.Points.Rrf;
|
||||
import java.util.List;
|
||||
|
||||
QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
|
||||
@@ -24,7 +24,7 @@ client.queryAsync(
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(fusion(Fusion.RRF))
|
||||
.setQuery(rrf(Rrf.newBuilder().build()))
|
||||
.build())
|
||||
.get();
|
||||
```
|
||||
|
||||
+1
-1
@@ -17,6 +17,6 @@ client.query_points(
|
||||
limit=20,
|
||||
),
|
||||
],
|
||||
query=models.FusionQuery(fusion=models.Fusion.RRF),
|
||||
query=models.RrfQuery(rrf=models.Rrf()),
|
||||
)
|
||||
```
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
```rust
|
||||
use qdrant_client::Qdrant;
|
||||
use qdrant_client::qdrant::{Fusion, PrefetchQueryBuilder, Query, QueryPointsBuilder};
|
||||
use qdrant_client::qdrant::{PrefetchQueryBuilder, Query, QueryPointsBuilder, RrfBuilder};
|
||||
|
||||
let client = Qdrant::from_url("http://localhost:6334").build()?;
|
||||
|
||||
@@ -16,6 +16,6 @@ client.query(
|
||||
.using("dense")
|
||||
.limit(20u64)
|
||||
)
|
||||
.query(Query::new_fusion(Fusion::Rrf))
|
||||
.query(Query::new_rrf(RrfBuilder::default()))
|
||||
).await?;
|
||||
```
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ client.query("{collection_name}", {
|
||||
},
|
||||
],
|
||||
query: {
|
||||
fusion: 'rrf',
|
||||
rrf: {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
+3
-1
@@ -20,12 +20,14 @@ func Main() {
|
||||
{
|
||||
Query: qdrant.NewQuerySparse([]uint32{1, 42}, []float32{0.22, 0.8}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDense([]float32{0.01, 0.45, 0.67}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFusion(qdrant.Fusion_RRF),
|
||||
Query: qdrant.NewQueryRRF(&qdrant.Rrf{}),
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ POST /collections/{collection_name}/points/query
|
||||
"limit": 20
|
||||
}
|
||||
],
|
||||
"query": { "fusion": "rrf" }, // <--- reciprocal rank fusion
|
||||
"query": { "rrf": {} }, // <--- reciprocal rank fusion with defaults
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
package com.example.snippets_amalgamation;
|
||||
|
||||
import static io.qdrant.client.QueryFactory.fusion;
|
||||
import static io.qdrant.client.QueryFactory.nearest;
|
||||
import static io.qdrant.client.QueryFactory.rrf;
|
||||
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.QdrantGrpcClient;
|
||||
import io.qdrant.client.grpc.Points.Fusion;
|
||||
import io.qdrant.client.grpc.Points.PrefetchQuery;
|
||||
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||
import io.qdrant.client.grpc.Points.Rrf;
|
||||
import java.util.List;
|
||||
|
||||
public class Snippet {
|
||||
@@ -27,7 +27,7 @@ public class Snippet {
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(fusion(Fusion.RRF))
|
||||
.setQuery(rrf(Rrf.newBuilder().build()))
|
||||
.build())
|
||||
.get();
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,5 +16,5 @@ client.query_points(
|
||||
limit=20,
|
||||
),
|
||||
],
|
||||
query=models.FusionQuery(fusion=models.Fusion.RRF),
|
||||
query=models.RrfQuery(rrf=models.Rrf()),
|
||||
)
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
use qdrant_client::Qdrant;
|
||||
use qdrant_client::qdrant::{Fusion, PrefetchQueryBuilder, Query, QueryPointsBuilder};
|
||||
use qdrant_client::qdrant::{PrefetchQueryBuilder, Query, QueryPointsBuilder, RrfBuilder};
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let client = Qdrant::from_url("http://localhost:6334").build()?;
|
||||
@@ -16,7 +16,7 @@ pub async fn main() -> anyhow::Result<()> {
|
||||
.using("dense")
|
||||
.limit(20u64)
|
||||
)
|
||||
.query(Query::new_fusion(Fusion::Rrf))
|
||||
.query(Query::new_rrf(RrfBuilder::default()))
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
+1
-1
@@ -19,6 +19,6 @@ client.query("{collection_name}", {
|
||||
},
|
||||
],
|
||||
query: {
|
||||
fusion: 'rrf',
|
||||
rrf: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Hybrid Queries
|
||||
short_description: "Combine dense, sparse, and multivector queries in Qdrant with hybrid search and multi-stage Universal Query pipelines."
|
||||
description: "Run hybrid and multi-stage queries in Qdrant — fuse dense, sparse, and multivector results with RRF or DBSF using the Universal Query API for hybrid search."
|
||||
short_description: "Combine dense, sparse, and multivector queries in Qdrant with hybrid search, weighted RRF tuning, DBSF, and multi-stage rescoring with Formula Query."
|
||||
description: "Run hybrid queries in Qdrant: fuse dense, sparse, and multivector results with RRF or DBSF, layer custom scoring with Formula Query, and pick the right method for your data."
|
||||
weight: 15
|
||||
aliases:
|
||||
- ../hybrid-queries
|
||||
@@ -53,6 +53,8 @@ Where:
|
||||
- $r_d$ is the rank of document $d$ in ranking $r$
|
||||
- $w_r$ is the weight of ranking $r$ (set to 1 by default)
|
||||
|
||||
_Qdrant uses zero-based rank positions; the top result has $r_d = 0$._
|
||||
|
||||
Because $w_r$ defaults to 1, without setting explicit weights, the formula can be simplified to the original RRF function:
|
||||
|
||||
$$ score(d\in D) = \sum_{r_d\in R(d)} \frac{1}{k + r_d} $$
|
||||
@@ -64,14 +66,14 @@ Here is an example of RRF for a query containing two prefetches against differen
|
||||
#### Setting RRF Constant k
|
||||
_Available as of v1.16.0_
|
||||
|
||||
To change the value of constant $k$ in the formula, use the dedicated `rrf` query.
|
||||
To set the constant $k$ in the formula:
|
||||
|
||||
{{< code-snippet path="/documentation/headless/snippets/query-points/hybrid-rrf-k/" >}}
|
||||
|
||||
#### Weighted RRF
|
||||
_Available as of v1.17.0_
|
||||
|
||||
By default, each query is assigned an equal weight. In reality, some queries are stronger, more discriminative, or more domain-specific than others. For example, a semantic search model understands meaning better than a simple keyword matcher. Assigning equal weight to both can cause the weaker model to negatively influence results, leading to a suboptimal search experience. To address this, you can assign greater weight to rankers that perform well.
|
||||
By default, each query is assigned an equal weight. In reality, one retriever is often stronger than the other for a given workload. For example, a dense retriever may dominate on natural-language queries, while BM25 may win on identifier-heavy ones. Assigning equal weight to both can let the weaker retriever drag down results. To address this, you can assign greater weight to rankers that perform well on your evaluation set.
|
||||
|
||||
The `rrf` query allows you to configure relative weights for each of the prefetches. For example, if you have two prefetches and assign a weight of 3.0 to the first and 1.0 to the second, a document ranked third in the first query scores the same as a document ranked first in the second query. In the case of non-overlapping result sets, these weights return three results from the first set for every one result from the second set.
|
||||
|
||||
@@ -79,18 +81,43 @@ Weights should be provided as an array of numbers, where each weight is applied
|
||||
|
||||
{{< code-snippet path="/documentation/headless/snippets/query-points/hybrid-rrf-weights/" >}}
|
||||
|
||||
Weights are a configuration choice, not something you can tune arbitrarily. The most reliable way to set them is by testing on your data.
|
||||
|
||||
- **With an eval set (queries paired with known-relevant docs):** split your eval queries in two. Try different weights on the first half, then measure on the second half. Measuring on the same queries you tuned on inflates the result. The [Choosing a Fusion Method notebook](https://githubtocolab.com/qdrant/examples/blob/master/fusion-methods/Choosing_a_Fusion_Method.ipynb) provides a reusable `tune_rrf_weights` grid-search helper you can adapt to a train/val split.
|
||||
- **Without an eval set:** leave weights at the default `(1.0, 1.0)`. Hand-tuned weights without measurement are unlikely to beat the default reliably.
|
||||
|
||||
Retune when your retrievers change (new embedding model, new chunking), when your corpus drifts substantially, or on a fixed cadence with a fresh eval sample.
|
||||
|
||||
### Distribution-Based Score Fusion (DBSF)
|
||||
|
||||
_Available as of v1.11.0_
|
||||
|
||||
<a href=https://medium.com/plain-simple-software/distribution-based-score-fusion-dbsf-a-new-approach-to-vector-search-ranking-f87c37488b18 target="_blank">
|
||||
DBSF</a>
|
||||
normalizes the scores of the points in each query, using the mean +/- the 3rd standard deviation as limits, and then sums the scores of the same point across different queries.
|
||||
|
||||
<aside role="status"><code>dbsf</code> is stateless and calculates the normalization limits only based on the results of each query, not on all the scores that it has seen.</aside>
|
||||
<a href=https://medium.com/plain-simple-software/distribution-based-score-fusion-dbsf-a-new-approach-to-vector-search-ranking-f87c37488b18 target="_blank">DBSF</a> keeps the raw scores from each query but normalizes their distributions before combining. For each retriever's returned set, it computes the mean $\mu$ and sample standard deviation $\sigma$, then normalizes every score using the 3-sigma extremes as endpoints:
|
||||
|
||||
$$ \hat{s} = \frac{s - (\mu - 3\sigma)}{6\sigma} $$
|
||||
|
||||
Normalized scores are summed across retrievers. Different score magnitudes no longer matter because each retriever contributes on the same comparable range.
|
||||
|
||||
<aside role="status"><code>dbsf</code> is stateless and computes its normalization limits from each query's returned points, not from all the scores it has seen. Scores are <strong>not</strong> clipped to [0, 1]; values outside the 3-sigma range remain outside it after the remap. If all returned scores are identical (or only one point is returned), DBSF emits <code>0.5</code> rather than dividing by zero.</aside>
|
||||
|
||||
DBSF is a reasonable choice when you trust your retrievers' raw scores to carry magnitude information. On well-calibrated retrievers DBSF can outperform tuned weighted RRF; on others weighted RRF wins. Neither dominates the other in general, so use your eval set to choose between them. Two caveats apply: the statistics come from the prefetch top-k (a small sample), and a single dominant outlier in that top-k can skew normalization for that query. Increase the prefetch `limit` if you see unstable rankings.
|
||||
|
||||
{{< code-snippet path="/documentation/headless/snippets/query-points/hybrid-dbsf/" >}}
|
||||
|
||||
### Choosing a Fusion Method
|
||||
|
||||
| If you have... | Use |
|
||||
| --- | --- |
|
||||
| An eval set (queries with known-relevant docs) to tune on | Weighted RRF, with weights tuned on a train/val split |
|
||||
| Trust in your retrievers' raw scores and no eval set | DBSF |
|
||||
| Neither an eval set nor strong score priors | RRF (the safe default) |
|
||||
|
||||
For a deeper breakdown of when to prefer each, see the [FAQ on RRF vs. DBSF](/documentation/faq/qdrant-fundamentals/#when-should-i-use-reciprocal-rank-fusion-rrf-vs-distribution-based-score-fusion-dbsf-for-hybrid-search). To layer business logic (recency, popularity, geo) on top of a fused result, see [Custom scoring with a formula query](#custom-scoring-with-a-formula-query).
|
||||
|
||||
<aside role="status">A common request is "alpha-weighted linear combination of dense and sparse scores." This is unreliable without first normalizing the scores: dense (cosine, bounded) and sparse (BM25, unbounded) scores live on different scales that also shift per query, so a fixed alpha over raw scores tends to be dominated by whichever retriever has larger raw magnitudes on a given query. RRF sidesteps this by using ranks. DBSF sidesteps it by normalizing distributions.</aside>
|
||||
|
||||
|
||||
## Multi-stage queries
|
||||
## Multi-Stage Queries
|
||||
|
||||
In general, larger vector representations give more accurate search results, but makes them more expensive to compute.
|
||||
|
||||
@@ -110,7 +137,7 @@ such that the coarse results are fetched first, and then they are refined later
|
||||
|
||||
<aside role="status">Disable the HNSW index for vectors used only for rescoring by setting <code>m=0</code> in the vector's HNSW configuration. Rescoring does not use the HNSW index, so disabling it will free up memory.</aside>
|
||||
|
||||
### Re-scoring examples
|
||||
### Re-Scoring Examples
|
||||
|
||||
Fetch 1000 results using a shorter MRL byte vector, then re-score them using the full vector and get the top 10.
|
||||
|
||||
@@ -120,10 +147,22 @@ Fetch 100 results using the default vector, then re-score them using a multi-vec
|
||||
|
||||
{{< code-snippet path="/documentation/headless/snippets/query-points/hybrid-rescoring-multivector/" >}}
|
||||
|
||||
It is possible to combine all the above techniques in a single query:
|
||||
You can combine all of these techniques in a single query:
|
||||
|
||||
{{< code-snippet path="/documentation/headless/snippets/query-points/hybrid-rescoring-multistage/" >}}
|
||||
|
||||
### Custom Scoring with a Formula Query
|
||||
|
||||
_Available as of v1.14.0_
|
||||
|
||||
A formula query lets you compose a final score from prefetch scores (`$score`), payload fields, and built-in helpers like exponential or Gaussian decay. The typical pattern is to fuse retrievers with RRF or DBSF in a prefetch, then wrap that prefetch in a formula query that layers ranking logic on top: recency decay, popularity boosts, geo decay, or category-conditional multipliers.
|
||||
|
||||
{{< code-snippet path="/documentation/headless/snippets/query-points/hybrid-formula-decay/" >}}
|
||||
|
||||
<aside role="status">Calibrate the decay weight against the scale of your fused <code>$score</code>. RRF scores are small (sums of <code>1/(k+rank)</code> terms), while decay functions return values in <code>[0, 1]</code>, so an unweighted decay term will dominate the fused score unless you multiply it by a smaller coefficient. Wrap the decay in a multiplication expression with a coefficient tuned to your workload.</aside>
|
||||
|
||||
The [Choosing a Fusion Method notebook](https://githubtocolab.com/qdrant/examples/blob/master/fusion-methods/Choosing_a_Fusion_Method.ipynb) shows this pattern end-to-end with exponential decay on a `published_at` payload field. For full formula query and decay function syntax, see the [Search Relevance reference](/documentation/search/search-relevance/).
|
||||
|
||||
## Grouping
|
||||
|
||||
_Available as of v1.11.0_
|
||||
|
||||
Reference in New Issue
Block a user