Search Relevance

By default, Qdrant ranks search results based on vector similarity scores. However, you may wish to consider additional factors when ranking results. Qdrant offers several tools to help you accomplish this.

Score Boosting

Available as of v1.14.0

When introducing vector search to specific applications, sometimes business logic needs to be considered for ranking the final list of results.

A quick example is our own documentation search bar. It has vectors for every part of the documentation site. If one were to perform a search by “just” using the vectors, all kinds of elements would be equally considered good results. However, when searching for documentation, we can establish a hierarchy of importance:

title > content > snippets

One way to solve this is to weight the results based on the kind of element. For example, we can assign a higher weight to titles and content and keep snippets unboosted.

Pseudocode would be something like:

score = score + (is_title * 0.5) + (is_content * 0.25)

The Query API can rescore points with custom formulas based on:

  • Dynamic payload values
  • Conditions
  • Scores of prefetches

To express the formula, the syntax uses objects to identify each element. Taking the documentation example, the request would look like this:

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("http://localhost: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")),
                    },
                }),
            },
        }),
    }),
})

There are multiple expressions available. Check the API docs for specific details.

  • constant - A floating point number. e.g. 0.5.
  • "$score" - Reference to the score of the point in the prefetch. This is the same as "$score[0]".
  • "$score[0]", "$score[1]", "$score[2]", … - When using multiple prefetches, you can reference specific prefetch with the index within the array of prefetches.
  • payload key - Any plain string will refer to a payload key. This uses the jsonpath format used in every other place, e.g. key or key.subkey. It will try to extract a number from the given key.
  • condition - A filtering condition. If the condition is met, it becomes 1.0, otherwise 0.0.
  • mult - Multiply an array of expressions.
  • sum - Sum an array of expressions.
  • max - Largest of an array of expressions. Available as of v1.19.1. See more.
  • min - Smallest of an array of expressions. Available as of v1.19.1. See more.
  • div - Divide an expression by another expression.
  • abs - Absolute value of an expression.
  • pow - Raise an expression to the power of another expression.
  • sqrt - Square root of an expression.
  • log10 - Base 10 logarithm of an expression.
  • ln - Natural logarithm of an expression.
  • acosh - Inverse hyperbolic cosine of an expression. The input must be 1 or greater. Available as of v1.19.1. See more.
  • exp - Exponential function of an expression (e^x).
  • geo distance - Haversine distance between two geographic points. Values need to be { "lat": 0.0, "lon": 0.0 } objects.
  • decay - Apply a decay function to an expression, which clamps the output between 0 and 1. Available decay functions are linear, exponential, and gaussian. See more.
  • datetime - Parse a datetime string (see formats here), and use it as a POSIX timestamp in seconds.
  • datetime key - Specify that a payload key contains a datetime string to be parsed into POSIX seconds.

It is possible to define a default for when the variable (either from payload or prefetch score) is not found. This is given in the form of a mapping from variable to value. If there is no variable and no defined default, a default value of 0.0 is used.

Max and Min

Available as of v1.19.1

max and min take an array of expressions and return the largest or the smallest value. An empty array returns an error.

Use max to search across several fields and keep each point’s best score. Run one prefetch per field. In this example, $score[0] and $score[1] refer to the scores from the first and second prefetch. It runs a BM25 text query against the title and body payload fields, and multiplies the title score by 3 to boost title matches:

POST /collections/{collection_name}/points/query
{
    "prefetch": [
        {
            "query": { "text": "vector database" },
            "using": "title",
            "limit": 50
        },
        {
            "query": { "text": "vector database" },
            "using": "body",
            "limit": 50
        }
    ],
    "query": {
        "formula": {
            "max": [
                { "mult": [3.0, "$score[0]"] },
                "$score[1]"
            ]
        }
    }
}
from qdrant_client import QdrantClient, models

best_fields = client.query_points(
    collection_name="{collection_name}",
    prefetch=[
        models.Prefetch(
            query=models.TextQuery(text="vector database"),
            using="title",
            limit=50,
        ),
        models.Prefetch(
            query=models.TextQuery(text="vector database"),
            using="body",
            limit=50,
        ),
    ],
    query=models.FormulaQuery(
        formula=models.MaxExpression(max=[
            models.MultExpression(mult=[3.0, "$score[0]"]),  # boost title matches
            "$score[1]",
        ])
    )
)
import { QdrantClient } from "@qdrant/js-client-rest";

const best_fields = await client.query("{collection_name}", {
  prefetch: [
    {
      query: { text: "vector database" },
      using: "title",
      limit: 50
    },
    {
      query: { text: "vector database" },
      using: "body",
      limit: 50
    }
  ],
  query: {
    formula: {
      max: [
        { mult: [3.0, "$score[0]"] }, // boost title matches
        "$score[1]"
      ]
    }
  }
});
use qdrant_client::qdrant::{
    Expression, FormulaBuilder, PrefetchQueryBuilder, Query, QueryPointsBuilder,
};
use qdrant_client::Qdrant;

let _best_fields = client
    .query(
        QueryPointsBuilder::new("{collection_name}")
            .add_prefetch(
                PrefetchQueryBuilder::default()
                    .query(Query::new_text("vector database"))
                    .using("title")
                    .limit(50u64),
            )
            .add_prefetch(
                PrefetchQueryBuilder::default()
                    .query(Query::new_text("vector database"))
                    .using("body")
                    .limit(50u64),
            )
            .query(FormulaBuilder::new(Expression::max_with([
                // boost title matches
                Expression::mult_with([
                    Expression::constant(3.0),
                    Expression::variable("$score[0]"),
                ]),
                Expression::variable("$score[1]"),
            ])))
            .limit(10),
    )
    .await?;
import static io.qdrant.client.ExpressionFactory.constant;
import static io.qdrant.client.ExpressionFactory.max;
import static io.qdrant.client.ExpressionFactory.mult;
import static io.qdrant.client.ExpressionFactory.variable;
import static io.qdrant.client.QueryFactory.formula;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.Formula;
import io.qdrant.client.grpc.Points.MaxExpression;
import io.qdrant.client.grpc.Points.MultExpression;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.Query;
import io.qdrant.client.grpc.Points.QueryPoints;
import io.qdrant.client.grpc.Points.TextQuery;

client
  .queryAsync(
    QueryPoints.newBuilder()
      .setCollectionName("{collection_name}")
      .addPrefetch(
        PrefetchQuery.newBuilder()
          .setQuery(
            Query.newBuilder()
              .setText(TextQuery.newBuilder().setQuery("vector database"))
              .build())
          .setUsing("title")
          .setLimit(50)
          .build())
      .addPrefetch(
        PrefetchQuery.newBuilder()
          .setQuery(
            Query.newBuilder()
              .setText(TextQuery.newBuilder().setQuery("vector database"))
              .build())
          .setUsing("body")
          .setLimit(50)
          .build())
      .setQuery(
        formula(
          Formula.newBuilder()
            .setExpression(
              max(
                MaxExpression.newBuilder()
                  // boost title matches
                  .addMax(
                    mult(
                      MultExpression.newBuilder()
                        .addMult(constant(3.0f))
                        .addMult(variable("$score[0]"))
                        .build()))
                  .addMax(variable("$score[1]"))
                  .build()))
            .build()))
      .build())
  .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

await client.QueryAsync(
    collectionName: "{collection_name}",
    prefetch:
    [
        new PrefetchQuery
        {
            Query = new Query { Text = new TextQuery { Query = "vector database" } },
            Using = "title",
            Limit = 50,
        },
        new PrefetchQuery
        {
            Query = new Query { Text = new TextQuery { Query = "vector database" } },
            Using = "body",
            Limit = 50,
        },
    ],
    query: new Formula
    {
        Expression = new MaxExpression
        {
            Max =
            {
                new MultExpression { Mult = { 3.0f, "$score[0]" } }, // boost title matches
                "$score[1]",
            },
        },
    },
    limit: 10
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client.Query(context.Background(), &qdrant.QueryPoints{
	CollectionName: "{collection_name}",
	Prefetch: []*qdrant.PrefetchQuery{
		{
			Query: qdrant.NewQueryText(&qdrant.TextQuery{Query: "vector database"}),
			Using: qdrant.PtrOf("title"),
			Limit: qdrant.PtrOf(uint64(50)),
		},
		{
			Query: qdrant.NewQueryText(&qdrant.TextQuery{Query: "vector database"}),
			Using: qdrant.PtrOf("body"),
			Limit: qdrant.PtrOf(uint64(50)),
		},
	},
	Query: qdrant.NewQueryFormula(&qdrant.Formula{
		Expression: qdrant.NewExpressionMax(&qdrant.MaxExpression{
			Max: []*qdrant.Expression{
				// boost title matches
				qdrant.NewExpressionMult(&qdrant.MultExpression{
					Mult: []*qdrant.Expression{
						qdrant.NewExpressionConstant(3.0),
						qdrant.NewExpressionVariable("$score[0]"),
					},
				}),
				qdrant.NewExpressionVariable("$score[1]"),
			},
		}),
	}),
})

Use min to limit how much a boost affects the ranking. This example adds a popularity boost of at most 2.0 to the score:

POST /collections/{collection_name}/points/query
{
    "prefetch": { "query": [0.2, 0.8, ...], "limit": 50 },
    "query": {
        "formula": {
            "sum": [
                "$score",
                { "min": [2.0, { "mult": [0.1, "log_views"] }] }
            ]
        }
    }
}
from qdrant_client import QdrantClient, models

capped_boost = 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.MinExpression(min=[
                2.0,  # the boost can't exceed this value
                models.MultExpression(mult=[0.1, "log_views"])
            ])
        ])
    )
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

const capped_boost = await client.query("{collection_name}", {
  prefetch: {
    query: [0.1, 0.45, 0.67],
    limit: 50
  },
  query: {
    formula: {
      sum: [
        "$score",
        // the boost can't exceed 2.0
        { min: [2.0, { mult: [0.1, "log_views"] }] }
      ]
    }
  }
});
use qdrant_client::qdrant::{
    Expression, FormulaBuilder, PrefetchQueryBuilder, QueryPointsBuilder,
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

let _capped_boost = 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::min_with([
                    // the boost can't exceed this value
                    Expression::constant(2.0),
                    Expression::mult_with([
                        Expression::constant(0.1),
                        Expression::variable("log_views"),
                    ]),
                ]),
            ])))
            .limit(10),
    )
    .await?;
import static io.qdrant.client.ExpressionFactory.constant;
import static io.qdrant.client.ExpressionFactory.min;
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.MinExpression;
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;

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(
                    min(
                      MinExpression.newBuilder()
                        .addMin(constant(2.0f)) // the boost can't exceed this value
                        .addMin(
                          mult(
                            MultExpression.newBuilder()
                              .addMult(constant(0.1f))
                              .addMult(variable("log_views"))
                              .build()))
                        .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.01f, 0.45f, 0.67f }, Limit = 100 },
    ],
    query: new Formula
    {
        Expression = new SumExpression
        {
            Sum =
            {
                "$score",
                new MinExpression
                {
                    Min =
                    {
                        2.0f, // the boost can't exceed this value
                        new MultExpression { Mult = { 0.1f, "log_views" } },
                    },
                },
            },
        },
    },
    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.NewExpressionMin(&qdrant.MinExpression{
					Min: []*qdrant.Expression{
						qdrant.NewExpressionConstant(2.0), // the boost can't exceed this value
						qdrant.NewExpressionMult(&qdrant.MultExpression{
							Mult: []*qdrant.Expression{
								qdrant.NewExpressionConstant(0.1),
								qdrant.NewExpressionVariable("log_views"),
							},
						}),
					},
				}),
			},
		}),
	}),
})

Combine max and min to keep a value (x) within a range: max(lo, min(hi, x)). This example clamps the payload value rating between 0 and 1:

POST /collections/{collection_name}/points/query
{
    "prefetch": { "query": [0.2, 0.8, ...], "limit": 50 },
    "query": {
        "formula": {
            "sum": [
                "$score",
                { "max": [0.0, { "min": [1.0, "rating"] }] }
            ]
        }
    }
}
from qdrant_client import QdrantClient, models

clamped = 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",
            # clamp `rating` into the range [0, 1]
            models.MaxExpression(max=[
                0.0,
                models.MinExpression(min=[1.0, "rating"])
            ])
        ])
    )
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

const clamped = await client.query("{collection_name}", {
  prefetch: {
    query: [0.1, 0.45, 0.67],
    limit: 50
  },
  query: {
    formula: {
      sum: [
        "$score",
        // clamp `rating` into the range [0, 1]
        { max: [0.0, { min: [1.0, "rating"] }] }
      ]
    }
  }
});
use qdrant_client::qdrant::{
    Expression, FormulaBuilder, PrefetchQueryBuilder, QueryPointsBuilder,
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

let _clamped = 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(),
                // clamp `rating` into the range [0, 1]
                Expression::max_with([
                    Expression::constant(0.0),
                    Expression::min_with([
                        Expression::constant(1.0),
                        Expression::variable("rating"),
                    ]),
                ]),
            ])))
            .limit(10),
    )
    .await?;
import static io.qdrant.client.ExpressionFactory.constant;
import static io.qdrant.client.ExpressionFactory.max;
import static io.qdrant.client.ExpressionFactory.min;
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.MaxExpression;
import io.qdrant.client.grpc.Points.MinExpression;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;
import io.qdrant.client.grpc.Points.SumExpression;

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"))
                  // clamp `rating` into the range [0, 1]
                  .addSum(
                    max(
                      MaxExpression.newBuilder()
                        .addMax(constant(0.0f))
                        .addMax(
                          min(
                            MinExpression.newBuilder()
                              .addMin(constant(1.0f))
                              .addMin(variable("rating"))
                              .build()))
                        .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.01f, 0.45f, 0.67f }, Limit = 100 },
    ],
    query: new Formula
    {
        Expression = new SumExpression
        {
            Sum =
            {
                "$score",
                // clamp `rating` into the range [0, 1]
                new MaxExpression
                {
                    Max = { 0.0f, new MinExpression { Min = { 1.0f, "rating" } } },
                },
            },
        },
    },
    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"),
				// clamp `rating` into the range [0, 1]
				qdrant.NewExpressionMax(&qdrant.MaxExpression{
					Max: []*qdrant.Expression{
						qdrant.NewExpressionConstant(0.0),
						qdrant.NewExpressionMin(&qdrant.MinExpression{
							Min: []*qdrant.Expression{
								qdrant.NewExpressionConstant(1.0),
								qdrant.NewExpressionVariable("rating"),
							},
						}),
					},
				}),
			},
		}),
	}),
})

Points Missing from a Prefetch

If a point is missing from a prefetch’s results, its score for that prefetch is 0.0. This can affect the result of max or min:

  • With max, the missing score wins over negative scores, such as negative dot product scores or negated Euclidean or Manhattan distances.
  • With min, the missing score wins over positive scores, including positive cosine scores.

Set a default for the missing prefetch score to keep it from winning the comparison. Use a value below the expected score range for max, such as -1e30, or above it for min, such as 1e30.

POST /collections/{collection_name}/points/query
{
    "prefetch": [
        { "query": [0.2, 0.8, ...], "using": "title", "limit": 50 },
        { "query": [0.2, 0.8, ...], "using": "summary", "limit": 50 }
    ],
    "query": {
        "formula": { "max": ["$score[0]", "$score[1]"] },
        "defaults": { "$score[1]": -1e30 }
    }
}
from qdrant_client import QdrantClient, models

best_match = client.query_points(
    collection_name="{collection_name}",
    prefetch=[
        models.Prefetch(query=[0.1, 0.45, 0.67], using="title", limit=50),
        models.Prefetch(query=[0.1, 0.45, 0.67], using="summary", limit=50),
    ],
    query=models.FormulaQuery(
        formula=models.MaxExpression(max=["$score[0]", "$score[1]"]),
        # used when a point is missing from the second prefetch
        defaults={"$score[1]": -1e30}
    )
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

const best_match = await client.query("{collection_name}", {
  prefetch: [
    { query: [0.1, 0.45, 0.67], using: "title", limit: 50 },
    { query: [0.1, 0.45, 0.67], using: "summary", limit: 50 }
  ],
  query: {
    formula: { max: ["$score[0]", "$score[1]"] },
    // used when a point is missing from the second prefetch
    defaults: { "$score[1]": -1e30 }
  }
});
use qdrant_client::qdrant::{
    Expression, FormulaBuilder, PrefetchQueryBuilder, QueryPointsBuilder,
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

let _best_match = client
    .query(
        QueryPointsBuilder::new("{collection_name}")
            .add_prefetch(
                PrefetchQueryBuilder::default()
                    .query(vec![0.01, 0.45, 0.67])
                    .using("title")
                    .limit(50u64),
            )
            .add_prefetch(
                PrefetchQueryBuilder::default()
                    .query(vec![0.01, 0.45, 0.67])
                    .using("summary")
                    .limit(50u64),
            )
            .query(
                FormulaBuilder::new(Expression::max_with([
                    Expression::variable("$score[0]"),
                    Expression::variable("$score[1]"),
                ]))
                // used when a point is missing from the second prefetch
                .add_default("$score[1]", -1e30),
            )
            .limit(10),
    )
    .await?;
import static io.qdrant.client.ExpressionFactory.max;
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.ValueFactory.value;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.Formula;
import io.qdrant.client.grpc.Points.MaxExpression;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;

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))
          .setUsing("title")
          .setLimit(50)
          .build())
      .addPrefetch(
        PrefetchQuery.newBuilder()
          .setQuery(nearest(0.01f, 0.45f, 0.67f))
          .setUsing("summary")
          .setLimit(50)
          .build())
      .setQuery(
        formula(
          Formula.newBuilder()
            .setExpression(
              max(
                MaxExpression.newBuilder()
                  .addMax(variable("$score[0]"))
                  .addMax(variable("$score[1]"))
                  .build()))
            // used when a point is missing from the second prefetch
            .putDefaults("$score[1]", value(-1e30))
            .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.01f, 0.45f, 0.67f }, Using = "title", Limit = 50 },
        new PrefetchQuery { Query = new float[] { 0.01f, 0.45f, 0.67f }, Using = "summary", Limit = 50 },
    ],
    query: new Formula
    {
        Expression = new MaxExpression { Max = { "$score[0]", "$score[1]" } },
        // used when a point is missing from the second prefetch
        Defaults = { ["$score[1]"] = -1e30 },
    },
    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),
			Using: qdrant.PtrOf("title"),
			Limit: qdrant.PtrOf(uint64(50)),
		},
		{
			Query: qdrant.NewQuery(0.01, 0.45, 0.67),
			Using: qdrant.PtrOf("summary"),
			Limit: qdrant.PtrOf(uint64(50)),
		},
	},
	Query: qdrant.NewQueryFormula(&qdrant.Formula{
		Expression: qdrant.NewExpressionMax(&qdrant.MaxExpression{
			Max: []*qdrant.Expression{
				qdrant.NewExpressionVariable("$score[0]"),
				qdrant.NewExpressionVariable("$score[1]"),
			},
		}),
		// used when a point is missing from the second prefetch
		Defaults: qdrant.NewValueMap(map[string]any{
			"$score[1]": -1e30,
		}),
	}),
})

Hyperbolic Distance

Available as of v1.19.1

The acosh expression returns the inverse hyperbolic cosine of a single expression, like sqrt, ln, and exp. Use it to rescore hyperbolic embeddings, which place hierarchical data in the Poincaré ball or on the Lorentz hyperboloid, by their exact geodesic distance.

The input must be at least 1, and acosh(1) returns 0. A smaller input returns a non-finite number error. Qdrant doesn’t clamp values that are only slightly below 1. If rounding could push the input below 1, add a small epsilon.

The formula you need depends on how you ingest and query the vectors. This example uses the Lorentz model with a curvature of 1:

  1. Create the collection with the Dot distance metric.
  2. Store the points as Lorentz-model vectors.
  3. Query with a vector of the same kind, but flip the sign of its first element.

The dot product, which is the $score, is then the Lorentzian inner product of the query and the point, and the geodesic distance is acosh(-$score). Formula results are sorted in descending order, so the example negates the distance to rank the closest points first:

POST /collections/{collection_name}/points/query
{
    "prefetch": {
        "query": [-1.118, 0.3, 0.4],
        "limit": 100
    },
    "query": {
        "formula": {
            "neg": { "acosh": { "neg": "$score" } }
        }
    }
}
from qdrant_client import QdrantClient, models

geodesic = client.query_points(
    collection_name="{collection_name}",
    prefetch=models.Prefetch(
        query=[-1.118, 0.3, 0.4],  # <-- the sign of the first element is flipped
        limit=100
    ),
    query=models.FormulaQuery(
        formula=models.NegExpression(
            neg=models.AcoshExpression(
                acosh=models.NegExpression(neg="$score")
            )
        )
    )
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

const geodesic = await client.query("{collection_name}", {
  prefetch: {
    query: [-1.118, 0.3, 0.4], // the sign of the first element is flipped
    limit: 100
  },
  query: {
    formula: {
      neg: { acosh: { neg: "$score" } }
    }
  }
});
use qdrant_client::qdrant::{
    Expression, FormulaBuilder, PrefetchQueryBuilder, QueryPointsBuilder,
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

let _geodesic = client
    .query(
        QueryPointsBuilder::new("{collection_name}")
            .add_prefetch(
                PrefetchQueryBuilder::default()
                    // the sign of the first element is flipped
                    .query(vec![-1.118, 0.3, 0.4])
                    .limit(100u64),
            )
            .query(FormulaBuilder::new(Expression::neg(Expression::acosh(
                Expression::neg(Expression::score()),
            ))))
            .limit(10),
    )
    .await?;
import static io.qdrant.client.ExpressionFactory.acosh;
import static io.qdrant.client.ExpressionFactory.negate;
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.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;

QdrantClient client =
  new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
  .queryAsync(
    QueryPoints.newBuilder()
      .setCollectionName("{collection_name}")
      .addPrefetch(
        PrefetchQuery.newBuilder()
          // the sign of the first element is flipped
          .setQuery(nearest(-1.118f, 0.3f, 0.4f))
          .setLimit(100)
          .build())
      .setQuery(
        formula(
          Formula.newBuilder()
            .setExpression(negate(acosh(negate(variable("$score")))))
            .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
        {
            // the sign of the first element is flipped
            Query = new float[] { -1.118f, 0.3f, 0.4f },
            Limit = 100,
        },
    ],
    query: new Formula
    {
        Expression = Expression.FromNeg(Expression.FromAcosh(Expression.FromNeg("$score"))),
    },
    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{
		{
			// the sign of the first element is flipped
			Query: qdrant.NewQuery(-1.118, 0.3, 0.4),
			Limit: qdrant.PtrOf(uint64(100)),
		},
	},
	Query: qdrant.NewQueryFormula(&qdrant.Formula{
		Expression: qdrant.NewExpressionNeg(
			qdrant.NewExpressionAcosh(
				qdrant.NewExpressionNeg(qdrant.NewExpressionVariable("$score")),
			),
		),
	}),
})

Decay Functions

Decay functions enable you to modify the score based on how far a value is from a target using a linear, exponential, or Gaussian decay function. For all decay functions, these are the available parameters:

ParameterDefaultDescription
xN/AThe value to decay
target0.0The value at which the decay will be at its peak. For distances, it is usually set at 0.0, but can be set to any value.
scale1.0The value at which the decay function will be equal to midpoint. This is in terms of x units. For example, if x is in meters, scale of 5000 means 5km. Must be a non-zero positive number.
midpoint0.5Output is midpoint when x equals target ± scale. Must be in the range (0.0, 1.0), exclusive.
Decay functions.
Each decay function maps x to a score in the range of 0 to 1. target, scale and midpoint determine the shape of the curve.

The formula for each decay function is as follows:


Decay FunctionRangeFormula
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)$

Boost Points Closer to User

An example of decay functions is to combine the score with how close a result is to a user.

Considering each point has an associated geo location, we can calculate the distance between the point and the request’s location.

Assuming we have cosine scores in the prefetch, we can use a helper function to clamp the geographical distance between 0 and 1, by using a decay function. Once clamped, we can sum the score and the distance together. Pseudocode:

score = score + gauss_decay(distance)

In this case, we use a gauss_decay function.

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("http://localhost: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,
            },
        }),
    }),
})

Time-Based Score Boosting

Or combine the score with the information on how “fresh” the result is. It’s applicable to (news) articles and, in general, many other different types of searches (think of the “newest” filter you use in applications).

To implement time-based score boosting, you’ll need each point to have a datetime field in its payload, e.g., when the item was uploaded or last updated. Then we can calculate the time difference in seconds between this payload value and the current time, our target.

With an exponential decay function, perfect for use cases with time, as freshness is a very quickly lost quality, we can convert this time difference into a value between 0 and 1, then add it to the original score to prioritize fresh results.

score = score + exp_decay(current_time - point_time)

That’s how it will look for an application where, after 1 day, results start being only half-relevant (so get a score of 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("http://localhost: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)),
                }),
            },
        }),
    }),
})

Maximal Marginal Relevance (MMR)

Available as of v1.15.0

Maximal Marginal Relevance (MMR) is an algorithm to improve the diversity of the results. It excels when the dataset has many redundant or very similar points for a query.

MMR selects candidates iteratively, starting with the most relevant point (higher similarity to the query). For each next point, it selects the one that hasn’t been chosen yet which has the best combination of relevance and higher separation to the already selected points.

$$ 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)] $$

Where $R$ is the candidates set, $S$ is the selected set, $Q$ is the query vector, $sim$ is the similarity function, and $\lambda = 1 - diversity$.

This is implemented in Qdrant as a parameter of a nearest neighbors query. You define the vector to get the nearest candidates, and a diversity parameter which controls the balance between relevance (0.0) and diversity (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="http://localhost: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("http://localhost: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)),
})

Caveat: Since MMR ranks one point at a time, the scores produced by MMR in Qdrant refer to the similarity to the query vector. This means that the response will not be ordered by score, but rather by the order of selection of MMR.

Relevance Feedback

Available as of 1.17

Relevance feedback distills signals from current search results into the next retrieval iteration to surface more relevant documents.

Qdrant provides a subtype of relevance feedback-based retrieval, where feedback is given by any model (relevance oracle) in a granular fashion: it rescores top retrieved results by their relative relevance to the query. A detailed overview of relevance feedback methods can be found in Relevance Feedback in Information Retrieval.

To use relevance feedback-based retrieval, two components are required:

  1. A collection of vectors to search through.
  2. An oracle to determine the relevance of search results.

The idea behind using relevance feedback-based retrieval is the following:

  1. Run a basic nearest neighbors search. Let’s call its results Retriever Similarity and the algorithm behind this search – retriever.
  2. Use any feedback model to assign a relevance score to the top X search results (X is not expected to be large, 3-5 is a good option). Let’s call these scores Feedback Score.
  3. Through analyzing the Feedback Score for the top results, determine if the feedback model agrees with the retriever, or if retrieval can be improved.
  4. If it can be improved, use feedback to modify retrieval (vector space traversal) to account for the discrepancies between the feedback model and the retriever.

For example, in this set of retrieved results:

Point IDRetriever SimilarityFeedback Score
1110.890.68
2220.810.72
3330.770.61

The feedback model considers the second result with ID 222 to be the most relevant, which is a discrepancy with the retriever’s ranking. Hence, this feedback can potentially help make the next iteration of retrieval better.


To leverage the feedback in search across the entire collection, Qdrant provides a query interface that requires:

  1. The original query (target), which can be a point ID, an inference object, or a raw vector.
  2. A short list of initial retrieval results and their relevance score (feedback). Each feedback item consists of:
    • example, which can be a point ID, an inference object, or a raw vector used by the retriever.
    • score, the feedback score.
  3. A definition of the formula that modifies retrieval based on the feedback (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,
			}),
		},
	),
})

Internally, Qdrant combines the feedback list into pairs, based on the relevance scores, and then uses these pairs in a formula that modifies vector space traversal during retrieval (changes the strategy of retrieval). This relevance feedback-based retrieval considers not only the similarity of candidates to the query but also to each feedback pair. For a more detailed description of how it works, refer to the article Relevance Feedback in Qdrant.

The a, b, and c parameters of the naive strategy need to be customized for each triplet of retriever, feedback model, and collection. To get these 3 weights adapted to your setup, use our open source Python package.

For a hands-on tutorial on determining the parameters, using them with the Relevance Feedback Query, and evaluating the results, check out Relevance Feedback in Qdrant.

Naive Strategy

For now, naive is the only available strategy.

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}

Was this page useful?

Thank you for your feedback! 🙏

We are sorry to hear that. 😔 You can edit this page on GitHub, or create a GitHub issue.