BM25 Document Ranker
knowledge.rank_bm25
Overview
Give us a query and the documents you want ranked, and we score each one with Okapi BM25 and return them best first. The formula and the analyzer are both published, so you can reproduce a score by hand: the non-negative inverse-document-frequency variant, and term splitting you control through a closed set of patterns rather than a regular expression. Corpus statistics come from the documents in your request and nowhere else, so a score is relative to the set you sent rather than an absolute measure of relevance. Nothing here is language-model tokenization and no tokenizer is involved: BM25 counts words, and subword units would give scores no BM25 reference agrees with. Documents matching nothing are counted rather than padding the ranking.
Lexical ranking is still the cheapest way to tell a relevant passage from a plausible one, and it only earns trust when the arithmetic is inspectable. Both the formula and the analyzer are published here, and the per-term breakdown is available on request, so a ranking you disagree with can be checked rather than argued with.
Reliability
Fully deterministic: the same query, documents and parameters always give the same scores and the same order, ties included, because ties end at the id. No clock, no randomness and no model. The formula is the standard non-negative variant, idf(q) = ln(1 + (N - n(q) + 0.5) / (n(q) + 0.5)), scored as the sum over query terms of idf(q) times f times (k1 + 1) divided by f plus k1 times (1 - b + b times dl over avgdl), with f the term frequency, dl the document length in terms and avgdl the mean across your documents. N and avgdl come from your request only, so the same document scores differently in a different set — that is inherent to BM25, not a quirk here. Scores are rounded to six decimals so a published example stays reproducible. Failures, none of which get better on their own: BM25_QUERY_INVALID means the query was empty, over 1000 characters, or analysed to more than 64 distinct terms; BM25_QUERY_NO_TERMS means the analyzer consumed the whole query, which usually means your stopword list or minTermLength was stricter than you intended; BM25_DOCUMENTS_INVALID means documents was empty, an entry was not an object, an id or text was missing or mistyped, or an entry carried a property we do not accept; BM25_DOCUMENT_ID_DUPLICATE means two documents share an id, and the error says which position repeated it; BM25_PARAMETER_INVALID means k1, b, titleBoost, limit or an analyzer setting was outside its range or named something that does not exist, and the accepted values come back with the error; BM25_LIMIT_EXCEEDED means more than 10000 documents, a document over 200000 characters, or a per-term breakdown that would exceed 20000 entries — lower limit or turn includeTermScores off; BM25_STOPWORD_LIMIT_EXCEEDED means more than 500 stopwords, which is already far more than a useful list needs.
Context footprint
At most 1000 ranked entries come back, each carrying an id, a score and the matched terms — never the document text. Per-term breakdowns are bounded at 20000 entries across the whole response. A successful call costs 1 execution credit.
When to use it
When you hold the documents and want them ordered by lexical relevance to a query, or want to rerank what a vector search gave you.
When you need semantic matching, when the documents live in an index you have not sent, or when you want scores comparable across separate requests.
Powered by Squillify's own implementation of the published Okapi BM25 formula with the non-negative inverse-document-frequency variant. No search engine, no third-party ranker and no model is involved.
Scope
Non-goals
- Does not use the platform tokenizer — BM25 counts words, not model tokens
- Does not hold an index or remember a document between calls
- Does not compile a regular expression you supply
- Does not ship a built-in stopword list for any language
- Does not compute embeddings or semantic similarity
Properties
Capabilities
Cost & billing
Flat rate per invocation
File handling
How we handle your data
What this means
This skill may receive sensitive content, but it does not target sensitive data and returns only a structured non-sensitive result.
Squillify guarantees
- Runs inside Squillify
- This skill runs inside Squillify.
- Payloads are not sent to an external service by this skill.
- Squillify does not permanently store submitted payloads.
- Telemetry records metadata only, not submitted content.
- Submitted payloads are not used for training.
- Submitted payloads are not reviewed by humans.
- This skill does not target sensitive data.
- The response does not contain submitted content.
Your responsibility
Squillify does not determine whether submitted content is sensitive. Your application is responsible for deciding what data it is allowed to submit. Your application is responsible for securing the response.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
| query | string | Yes | Query of 1 to 1000 characters that must analyse to between 1 and 64 distinct terms. |
| documents | array | Yes | From 1 through 10000 closed documents {id, text, optional title} over which corpus statistics are computed. |
| k1 | number | No | Term-frequency saturation from 0 through 3; omit it for 1.2. |
| b | number | No | Length-normalisation strength from 0 through 1; omit it for 0.75. |
| analyzer | object | No | Closed analyzer applied to query, documents and stopwords alike; omit it for NFKC Unicode words with case-folding, no stopwords and minTermLength 1. |
| titleBoost | number | No | Title term-frequency multiplier from 0 through 10; omit it for 0, which ignores titles. |
| limit | integer | No | How many ranked documents come back, from 1 through 1000; omit it for 100. |
| includeTermScores | boolean | No | Whether to attach per-term idf, frequency and contribution; omit it for false. |
Output schema
| Field | Type | Required | Description |
|---|---|---|---|
| ranked | array | Yes | Matching documents best first, each a closed {rank, id, score, matchedTerms} object with optional termScores; empty when nothing matched. |
| summary | object | Yes | Closed corpus and analyzer receipt with documentCount, query terms, length statistic, match counts, k1, b, titleBoost and truncation. |
API contract
POST /v1/executecurl -sS -X POST "$BASE/v1/execute" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"skillKey":"knowledge.rank_bm25","input":{"query":"refund policy","documents":[{"id":"d1","text":"Our refund policy covers 30 days."},{"id":"d2","text":"Shipping policy and delivery times."}],"k1":1.2,"b":0.75,"limit":10}}'import { Squillify } from "@squillify/sdk";
const sq = new Squillify({ apiKey: process.env.SQUILLIFY_API_KEY });
const { output } = await sq.execute("knowledge.rank_bm25", {"query":"refund policy","documents":[{"id":"doc-refunds","text":"Our refund policy: we refund an unused subscription within 30 days."},{"id":"doc-shipping","text":"Shipping takes three days and the policy covers lost parcels."},{"id":"doc-privacy","text":"We store personal data only as long as the law requires."}]});Errors
| Code | HTTP | Retryable | Description |
|---|---|---|---|
| VALIDATION_ERROR | 400 | No | The request payload failed schema validation. |
| UNAUTHORIZED | 401 | No | Missing or invalid API key. |
| SKILL_NOT_ACCESSIBLE | 403 | No | The API key is not granted access to this skill. |
| SKILL_NOT_AVAILABLE_FOR_PLAN | 403 | No | This skill is not included in your current plan. |
| PLAN_ELIGIBILITY_UNAVAILABLE | 503 | Yes | Plan eligibility could not be decided. Retry after backoff. |
| SKILL_NOT_FOUND | 404 | No | The requested skill or version does not exist. |
| TENANT_RATE_LIMITED | 429 | Yes | Request rate limit exceeded. Retry after backoff. |
| QUOTA_EXCEEDED | 429 | Yes | Plan quota exhausted. Retryable once the quota period resets. |
| EXECUTION_TIMEOUT | 504 | Yes | Execution exceeded the allowed time budget. |
| BM25_QUERY_INVALID | 400 | No | The request payload failed this skill's input validation. |
| BM25_QUERY_NO_TERMS | 412 | No | The input is valid but cannot be processed as-is. |
| BM25_DOCUMENTS_INVALID | 400 | No | The request payload failed this skill's input validation. |
| BM25_DOCUMENT_ID_DUPLICATE | 400 | No | The request payload failed this skill's input validation. |
| BM25_PARAMETER_INVALID | 400 | No | The request payload failed this skill's input validation. |
| BM25_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
| BM25_STOPWORD_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
Other limits
| Limit | Value |
|---|---|
| Recommended inline bytes | 32 KiB |
Limitations
- Does not use the platform tokenizer — BM25 counts words, not model tokens
- Does not hold an index or remember a document between calls
- Does not compile a regular expression you supply
- Does not ship a built-in stopword list for any language
- Does not compute embeddings or semantic similarity
- Searching a corpus you have not sent — there is no index here
- Semantic matching where wording differs but meaning does not
- Comparing scores between two different requests, which BM25 does not support
How to call
POST /v1/execute (requires authentication)
{
"skillKey": "knowledge.rank_bm25",
"input": {
"query": "refund policy",
"documents": [
{
"id": "doc-refunds",
"text": "Our refund policy: we refund an unused subscription within 30 days."
},
{
"id": "doc-shipping",
"text": "Shipping takes three days and the policy covers lost parcels."
},
{
"id": "doc-privacy",
"text": "We store personal data only as long as the law requires."
}
]
}
}Examples
Three documents ranked against a two-word query
{
"query": "refund policy",
"documents": [
{
"id": "doc-refunds",
"text": "Our refund policy: we refund an unused subscription within 30 days."
},
{
"id": "doc-shipping",
"text": "Shipping takes three days and the policy covers lost parcels."
},
{
"id": "doc-privacy",
"text": "We store personal data only as long as the law requires."
}
]
}{
"summary": {
"documentCount": 3,
"queryTerms": [
"refund",
"policy"
],
"droppedQueryTerms": [],
"averageDocumentLength": 10.666667,
"matchedCount": 2,
"unmatchedCount": 1,
"k1": 1.2,
"b": 0.75,
"titleBoost": 0,
"analyzer": {
"caseFold": true,
"unicodeForm": "NFKC",
"tokenPattern": "UNICODE_WORD",
"stopwordCount": 0,
"minTermLength": 1
},
"truncated": false
},
"ranked": [
{
"rank": 1,
"id": "doc-refunds",
"score": 1.800961,
"matchedTerms": [
"refund",
"policy"
]
},
{
"rank": 2,
"id": "doc-shipping",
"score": 0.482336,
"matchedTerms": [
"policy"
]
}
]
}Stopwords, a boosted title and length normalisation switched off
{
"analyzer": {
"stopwords": [
"the",
"of",
"a"
],
"minTermLength": 2
},
"query": "the refund of a subscription",
"documents": [
{
"text": "Refund requests are reviewed within two days.",
"title": "Refund handling",
"id": "a"
},
{
"id": "b",
"text": "A subscription renews monthly until it is cancelled by the customer."
}
],
"includeTermScores": true,
"b": 0,
"titleBoost": 2
}{
"summary": {
"documentCount": 2,
"queryTerms": [
"refund",
"subscription"
],
"droppedQueryTerms": [
"the",
"of",
"a"
],
"averageDocumentLength": 8,
"matchedCount": 2,
"unmatchedCount": 0,
"k1": 1.2,
"b": 0,
"titleBoost": 2,
"analyzer": {
"caseFold": true,
"unicodeForm": "NFKC",
"tokenPattern": "UNICODE_WORD",
"stopwordCount": 3,
"minTermLength": 2
},
"truncated": false
},
"ranked": [
{
"rank": 1,
"id": "a",
"score": 1.089231,
"matchedTerms": [
"refund"
],
"termScores": [
{
"term": "refund",
"idf": 0.693147,
"termFrequency": 3,
"contribution": 1.089231
}
]
},
{
"rank": 2,
"id": "b",
"score": 0.693147,
"matchedTerms": [
"subscription"
],
"termScores": [
{
"term": "subscription",
"idf": 0.693147,
"termFrequency": 1,
"contribution": 0.693147
}
]
}
]
}Recommended use cases
- Rerank a shortlist a vector search returned, using lexical evidence
- Rank a small document set without standing up a search engine
- Explain why one document outranks another, with per-term contributions
Avoid for
- Searching a corpus you have not sent — there is no index here
- Semantic matching where wording differs but meaning does not
- Comparing scores between two different requests, which BM25 does not support
Related skills
Composable with
Safety & privacy
- Runs without calling any external service.
- Your payloads are never stored permanently.
- Telemetry captures metadata only — never your payload contents.
- We never train models on your payloads.
- No human reviews your payloads.