--- title: BM25 Scoring description: BM25 scores sort the result set by relevance canonical: https://www.paradedb.com/docs/reference/full-text/score --- For optimal performance on top K queries ordered by BM25 score, consider setting `pnorms=true` when creating your index. See [Faster BM25 Queries](/reference/indexing/faster-bm25-queries) for setup and tradeoffs. Available in versions `0.26.0` and above. BM25 scores measure how relevant a document is for a given query. Higher scores indicate higher relevance. To adjust term frequency saturation and length normalization per field, see [BM25 Parameters](/reference/indexing/create-index#bm25-parameters). ## Basic Usage The `pdb.score()` function produces a BM25 score and can be added to any query where any of the ParadeDB operators are present. ```sql SQL SELECT id, pdb.score(id) FROM mock_items WHERE description ||| 'shoes' ORDER BY pdb.score(id) DESC LIMIT 5; ``` ```ts Drizzle import { desc } from "drizzle-orm"; import { search } from "@paradedb/drizzle-paradedb"; await db .select({ id: mockItems.id, score: search.score(mockItems.id), }) .from(mockItems) .where(search.matchAny(mockItems.description, "shoes")) .orderBy(desc(search.score(mockItems.id))) .limit(5); ``` ```python Django from paradedb import MatchAny, ParadeDB, Score MockItem.objects.filter( description=ParadeDB(MatchAny('shoes')) ).annotate( score=Score() ).values('id', 'score').order_by('-score')[:5] ``` ```python SQLAlchemy from sqlalchemy import desc, select from sqlalchemy.orm import Session from paradedb.sqlalchemy import pdb, search stmt = ( select(MockItem.id, pdb.score(MockItem.id).label("score")) .where(search.match_any(MockItem.description, "shoes")) .order_by(desc("score")) .limit(5) ) with Session(engine) as session: session.execute(stmt).all() ``` ```ruby Rails MockItem.search(:description) .match_any("shoes") .with_score .select(:id) .order(search_score: :desc) .limit(5) ``` ```cs EF Core await dbContext .MockItems.Where(item => EF.Functions.MatchAny(item.Description, "shoes")) .Select(item => new { item.Id, Score = EF.Functions.Score(item.Id) }) .OrderByDescending(item => item.Score) .Take(5) .ToListAsync(); ``` In order for a field to be factored into the BM25 score, it must be present in the ParadeDB index. For instance, consider this query: ```sql SQL SELECT id, pdb.score(id) FROM mock_items WHERE description ||| 'keyboard' OR rating < 2 ORDER BY pdb.score(id) DESC LIMIT 5; ``` ```ts Drizzle import { desc, lt, or } from "drizzle-orm"; import { search } from "@paradedb/drizzle-paradedb"; await db .select({ id: mockItems.id, score: search.score(mockItems.id), }) .from(mockItems) .where( or( search.matchAny(mockItems.description, "keyboard"), lt(mockItems.rating, 2), ), ) .orderBy(desc(search.score(mockItems.id))) .limit(5); ``` ```python Django from django.db.models import Q from paradedb import MatchAny, ParadeDB, Score MockItem.objects.filter( Q(description=ParadeDB(MatchAny('keyboard'))) | Q(rating__lt=2) ).annotate( score=Score() ).values('id', 'score').order_by('-score')[:5] ``` ```python SQLAlchemy from sqlalchemy import desc, or_, select from sqlalchemy.orm import Session from paradedb.sqlalchemy import pdb, search stmt = ( select(MockItem.id, pdb.score(MockItem.id).label("score")) .where(or_(search.match_any(MockItem.description, "keyboard"), MockItem.rating < 2)) .order_by(desc("score")) .limit(5) ) with Session(engine) as session: session.execute(stmt).all() ``` ```ruby Rails MockItem.search(:description) .match_any("keyboard") .or(MockItem.where(rating: ...2)) .with_score .select(:id) .order(search_score: :desc) .limit(5) ``` ```cs EF Core await dbContext .MockItems.Where(item => EF.Functions.MatchAny(item.Description, "keyboard") || item.Rating < 2 ) .Select(item => new { item.Id, Score = EF.Functions.Score(item.Id) }) .OrderByDescending(item => item.Score) .Take(5) .ToListAsync(); ``` While BM25 scores will be returned as long as `description` is indexed, including `rating` in the ParadeDB index definition will allow results matching `rating < 2` to rank higher than those that do not match. ## Joined Scores First, let's create a second table called `orders` that can be joined with `mock_items`: ```sql CALL paradedb.create_bm25_test_table( schema_name => 'public', table_name => 'orders', table_type => 'Orders' ); ALTER TABLE orders ADD CONSTRAINT foreign_key_product_id FOREIGN KEY (product_id) REFERENCES mock_items(id); CREATE INDEX orders_idx ON orders USING paradedb (order_id, product_id, order_quantity, order_total, customer_name) WITH (key_field = 'order_id'); ``` Next, let's compute a combined BM25 score over a join across both tables: ```sql SQL SELECT o.order_id, o.customer_name, m.description, pdb.score(o.order_id) + pdb.score(m.id) AS score FROM orders o JOIN mock_items m ON o.product_id = m.id WHERE o.customer_name ||| 'Johnson' OR m.description ||| 'running shoes' ORDER BY score DESC, o.order_id LIMIT 5; ``` ```ts Drizzle import { desc, eq, or, sql } from "drizzle-orm"; import { search } from "@paradedb/drizzle-paradedb"; const score = sql`${search.score(orders.orderId)} + ${search.score(mockItems.id)}`.as( "score", ); const results = await db .select({ orderId: orders.orderId, customerName: orders.customerName, description: mockItems.description, score, }) .from(orders) .innerJoin(mockItems, eq(orders.productId, mockItems.id)) .where( or( search.matchAny(orders.customerName, "Johnson"), search.matchAny(mockItems.description, "running shoes"), ), ) .orderBy(desc(score), orders.orderId) .limit(5); ``` ```python Django from django.db.models import F, Q from paradedb import MatchAny, ParadeDB, Score results = list( Order.objects.filter( Q(customer_name=ParadeDB(MatchAny("Johnson"))) | Q(product__description=ParadeDB(MatchAny("running shoes"))) ) .annotate( score=Score("order_id") + Score("product__description"), description=F("product__description"), ) .values("order_id", "customer_name", "description", "score") .order_by("-score", "order_id")[:5] ) ``` ```python SQLAlchemy from sqlalchemy import or_, select from sqlalchemy.orm import Session from paradedb.sqlalchemy import pdb, search score = (pdb.score(Order.order_id) + pdb.score(MockItem.id)).label("score") stmt = ( select(Order.order_id, Order.customer_name, MockItem.description, score) .join(MockItem, MockItem.id == Order.product_id) .where( or_( search.match_any(Order.customer_name, "Johnson"), search.match_any(MockItem.description, "running shoes"), ) ) .order_by(score.desc(), Order.order_id) .limit(5) ) with Session(engine) as session: results = session.execute(stmt).all() ``` ```ruby Rails orders = Order.arel_table products = MockItem.arel_table score = Arel::Nodes::Addition.new( Arel::Nodes::NamedFunction.new("pdb.score", [orders[:order_id]]), Arel::Nodes::NamedFunction.new("pdb.score", [products[:id]]) ) results = Order.joins(:mock_item) .search(:customer_name).match_any("Johnson") .or(Order.joins(:mock_item).search(products[:description]).match_any("running shoes")) .select(orders[:order_id], orders[:customer_name], products[:description], score.as("score")) .order(score.desc, orders[:order_id]) .limit(5).to_a ``` ```cs EF Core var results = await ( from order in dbContext.Orders join item in dbContext.MockItems on order.ProductId equals item.Id where EF.Functions.MatchAny(order.CustomerName, "Johnson") || EF.Functions.MatchAny(item.Description, "running shoes") let score = EF.Functions.Score(order.OrderId) + EF.Functions.Score(item.Id) orderby score descending, order.OrderId select new { order.OrderId, order.CustomerName, item.Description, Score = score, } ).Take(5).ToListAsync(); ``` To combine full text and vector search, where scores operate on different scales, see [Reciprocal Rank Fusion](/reference/hybrid/rrf). ## Deterministic Sorting Ordering by `pdb.score` alone is not sufficient to guarantee deterministic query results when there are multiple documents with the same score. To ensure stable output, we recommend adding a tiebreaker column (such as the primary key) after the score: ```sql SELECT id, pdb.score(id) FROM mock_items WHERE description ||| 'shoes' ORDER BY pdb.score(id) DESC, id ASC LIMIT 5; ``` Note that to receive this [Top K optimization](/reference/full-text/top-k), all tiebreaker columns must be indexed. ## Score Refresh The scores generated by the ParadeDB index may be influenced by dead rows that have not been cleaned up by the `VACUUM` process. Running `VACUUM` on the underlying table will remove all dead rows from the index and ensures that only rows visible to the current transaction are factored into the BM25 score. ```sql VACUUM mock_items; ``` This can be automated with [autovacuum](/operate/performance-tuning/overview).