The problem
BM25 is the classic way to rank documents for a query by the words they contain. It needs no training, it is fast, and it is hard to beat when the query contains exact names, codes or rare words. You will implement it as a class that indexes a collection of documents and scores queries against it.
The formula
where:
- runs over the terms of the query (a term that appears twice in the query counts twice);
- is the number of times occurs in document ;
- is the document's length relative to the average length;
- and are constants (defaults and );
- is Lucene's inverse document frequency, with documents of which contain : the rarer the term, the larger.
Two effects: saturation (more occurrences help less and less, controlled by ) and length normalization (the same count means less in a long document, controlled by ).
Example. Documents: =
["the", "cat", "sat"], =["the", "dog", "barked", "at", "the", "cat", "all", "day"], =["a", "bird", "sang"]. , lengths , average .
- Query
["cat"]: , . For : , , score . For : , , score . For : 0. Ranking: .- Query
["dog"]: , ; only : .- Query
["cat", "dog"]: the sum of the two: , , .- A term in no document, such as
"fish", adds 0.
What to implement
class BM25 with:
__init__(self, docs, k1=1.2, b=0.75):docsis a list of documents, each a list of tokens (strings). Precompute what the scores need: the count of every term in every document, the document lengths, the average length, and the document frequency of every term (in how many documents it appears).score(self, query):queryis a list of tokens. Return a NumPy array with one score per document.rank(self, query, k): the indices of thekbest documents, best first, ties by lower index.
Requirements
- is the number of times occurs in document (
collections.Counterper document). - Use the natural logarithm (
math.log).