---
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).