Skip to main content

Squillify development is paused indefinitely. Read why, and discover what this experiment achieved. Read the story

MicroSkills Reference

BM25 Document Ranker

knowledge.rank_bm25

ProductionBounded custom (Class B)Deterministicstrictmoderatev1.0.0
No network

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

Note
Score the documents in one request against one query with Okapi BM25 and return them in descending score order.

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

IdempotentRetry safeStreaming supportedFile input supportedRequires external service

Capabilities

analysissearchtext-processing

Cost & billing

Flat rate per invocation

MetercallsBillable onsuccessCost per call1 credit

File handling

Accepts file_refProduces file_refMax input2 MB

How we handle your data

Sensitive input, safe output
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.

Read the Microskill Data Handling Policy

Input schema

FieldTypeRequiredDescription
querystringYesQuery of 1 to 1000 characters that must analyse to between 1 and 64 distinct terms.
documentsarrayYesFrom 1 through 10000 closed documents {id, text, optional title} over which corpus statistics are computed.
k1numberNoTerm-frequency saturation from 0 through 3; omit it for 1.2.
bnumberNoLength-normalisation strength from 0 through 1; omit it for 0.75.
analyzerobjectNoClosed analyzer applied to query, documents and stopwords alike; omit it for NFKC Unicode words with case-folding, no stopwords and minTermLength 1.
titleBoostnumberNoTitle term-frequency multiplier from 0 through 10; omit it for 0, which ignores titles.
limitintegerNoHow many ranked documents come back, from 1 through 1000; omit it for 100.
includeTermScoresbooleanNoWhether to attach per-term idf, frequency and contribution; omit it for false.

Output schema

FieldTypeRequiredDescription
rankedarrayYesMatching documents best first, each a closed {rank, id, score, matchedTerms} object with optional termScores; empty when nothing matched.
summaryobjectYesClosed corpus and analyzer receipt with documentCount, query terms, length statistic, match counts, k1, b, titleBoost and truncation.

API contract

POST /v1/execute
curl
curl -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}}'
Integrations
TypeScript SDK
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

CodeHTTPRetryableDescription
VALIDATION_ERROR400NoThe request payload failed schema validation.
UNAUTHORIZED401NoMissing or invalid API key.
SKILL_NOT_ACCESSIBLE403NoThe API key is not granted access to this skill.
SKILL_NOT_AVAILABLE_FOR_PLAN403NoThis skill is not included in your current plan.
PLAN_ELIGIBILITY_UNAVAILABLE503YesPlan eligibility could not be decided. Retry after backoff.
SKILL_NOT_FOUND404NoThe requested skill or version does not exist.
TENANT_RATE_LIMITED429YesRequest rate limit exceeded. Retry after backoff.
QUOTA_EXCEEDED429YesPlan quota exhausted. Retryable once the quota period resets.
EXECUTION_TIMEOUT504YesExecution exceeded the allowed time budget.
BM25_QUERY_INVALID400NoThe request payload failed this skill's input validation.
BM25_QUERY_NO_TERMS412NoThe input is valid but cannot be processed as-is.
BM25_DOCUMENTS_INVALID400NoThe request payload failed this skill's input validation.
BM25_DOCUMENT_ID_DUPLICATE400NoThe request payload failed this skill's input validation.
BM25_PARAMETER_INVALID400NoThe request payload failed this skill's input validation.
BM25_LIMIT_EXCEEDED429NoA size, quota, rate, or time limit was exceeded.
BM25_STOPWORD_LIMIT_EXCEEDED429NoA size, quota, rate, or time limit was exceeded.

Other limits

LimitValue
Recommended inline bytes32 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)

Request bodyjson
{
  "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

Inputjson
{
  "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."
    }
  ]
}
Expected outputjson
{
  "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"
      ]
    }
  ]
}
Tip
The refund document matches both terms and ranks first; the shipping document matches only 'policy' and follows it; the privacy document matches nothing, so it is left out and counted as unmatched.

Stopwords, a boosted title and length normalisation switched off

Inputjson
{
  "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
}
Expected outputjson
{
  "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
        }
      ]
    }
  ]
}
Tip
The three stopwords are reported as dropped rather than scored, the title match counts double, b at 0 removes the length penalty, and the per-term breakdown shows exactly where each score came from.
  • 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

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.

Tags

retrievaltextrankingsearch