bm25s Documentation

repository·main·Indexed 23 days ago

https://github.com/xhluca/bm25s

An ultrafast, pure Python implementation of the BM25 ranking function powered by Numpy. It features eager sparse scoring for high-throughput lexical search, support for multiple BM25 variants (Lucene, Robertson, ATIRE, BM25L, BM25+), and memory-mapped files for large-scale retrieval. The library provides a high-level API for searching local files (.csv, .json, .jsonl, .txt), a comprehensive CLI for indexing and querying, and integration with Hugging Face Hub via BM25HF.

Tokens
7.3K
Snippets
16
Records
27
Agent score
76%

What's inside bm25s

  1. Compare bm25s performance and disk usage

    main

    The bm25s implementation is designed for high throughput and low disk footprint. It is a pure Python implementation powered by Numpy and sparse matrices.

    Performance Highlights:

    • Throughput: Often achieves significantly higher queries per second (QPS) than rank-bm25 and elasticsearch on various datasets (e.g., >500 QPS on arguana and fiqa).
    • Disk Usage: The base package is very lightweight (~51MB in a venv). Installing core dependencies (stemming, JIT, etc.) increases this to approximately 188MB, which is still much smaller than elasticsearch or bm25_pt.

    Memory Efficiency: Using memory-mapping (mmap=True) can reduce RAM usage by an order of magnitude (e.g., from ~4.4GB down to ~0.5GB for the NQ dataset).

  2. How corpus formats work in bm25s

    main

    bm25s separates the text used for indexing from the values returned during retrieval. You pass strings to bm25s.tokenize(), and then pass the resulting tokens to retriever.index().

    To control what is returned during a search, use the corpus argument in BM25(...), retrieve(...), or save(...). This argument can be a list of plain strings or a list of dictionaries (metadata).

    Using Metadata (Dictionaries)

    If your corpus contains dictionaries, retrieval is position-based: corpus[i] is returned for document ID i.

    text_corpus = [
        "a cat is a feline and likes to purr",
        "a dog is the human's best friend and loves to play",
    ]
    
    metadata_corpus = [
        {"id": "cat-doc", "title": "About Cat", "text": text_corpus[0]},
        {"id": "dog-doc", "title": "About Dog", "text": text_corpus[1]},
    ]
    
    # Index the text field
    corpus_tokens = bm25s.tokenize([doc["text"] for doc in metadata_corpus])
    
    # Pass the metadata as the corpus so retrieval returns the full dictionary
    retriever = bm25s.BM25(corpus=metadata_corpus)
    retriever.index(corpus_tokens)

    Note on Serialization: When saving, corpus entries must be strings, dictionaries, lists, or tuples that can be serialized to JSON. String entries are written to corpus.jsonl as {"id": i, "text": doc}.

    text_corpus = [
        "a cat is a feline and likes to purr",
        "a dog is the human's best friend and loves to play",
    ]
    
    metadata_corpus = [
        {"id": "cat-doc", "title": "About Cat", "text": text_corpus[0]},
        {"id": "dog-doc", "title": "About Dog", "text": text_corpus[1]},
    ]
    
    # Pick the text field(s) you want to index.
    corpus_tokens = bm25s.tokenize([doc["text"] for doc in metadata_corpus])
    
    # Retrieved documents will be the original dictionary entries.
    retriever = bm25s.BM25(corpus=metadata_corpus)
    retriever.index(corpus_tokens)
  3. Optimize RAM usage with memory-mapping

    main

    To handle large datasets that exceed available RAM, bm25s supports memory-mapping (mmap). This allows the index to be stored on disk and loaded on demand, significantly reducing the memory footprint compared to loading the entire index into memory.

    Key strategies for memory optimization:

    • Memory-mapped loading: Set mmap=True when loading/retrieving to use the index as a memory-mapped file. This reduces RAM usage during the indexing phase and the retrieval phase.
    • Mmap+Reload: For extremely large indices, use a batching approach (as seen in examples/retrieve_nq_with_batching.py) to reload the index after each batch. This keeps RAM usage consistently low by preventing accumulation during long retrieval processes.
  4. Quickstart: Create, index, and query a corpus with bm25s

    main

    This example demonstrates the standard workflow: creating a corpus, tokenizing it (optionally with a stemmer), indexing it with a BM25 model, and performing queries.

    import bm25s
    import Stemmer  # optional: for stemming
    
    # 1. Create your corpus
    corpus = [
        "a cat is a feline and likes to purr",
        "a dog is the human's best friend and loves to play",
        "a bird is a beautiful animal that can fly",
        "a fish is a creature that lives in water and swims",
    ]
    
    # 2. Optional: create a stemmer
    stemmer = Stemmer.Stemmer("english")
    
    # 3. Tokenize the corpus (keeping only IDs is faster and saves memory)
    corpus_tokens = bm25s.tokenize(corpus, stopwords="en", stemmer=stemmer)
    
    # 4. Create the BM25 model and index the corpus
    retriever = bm25s.BM25()
    retriever.index(corpus_tokens)
    
    # 5. Query the corpus
    query = "does the fish purr like a cat?"
    query_tokens = bm25s.tokenize(query, stemmer=stemmer)
    
    # 6. Get top-k results as a tuple of (doc ids, scores).
    # Both are arrays of shape (n_queries, k).
    # To return docs instead of IDs, set the `corpus=corpus` parameter in retriever.retrieve.
    results, scores = retriever.retrieve(query_tokens, k=2)
    
    for i in range(results.shape[1]):
        doc, score = results[0, i], scores[0, i]
        print(f"Rank {i+1} (score: {score:.2f}): {doc}")
    
    # 7. Save and Load
    retriever.save("animal_index_bm25")
    # To save with the corpus: retriever.save("animal_index_bm25", corpus=corpus)
    
    # Reloading
    reloaded_retriever = bm25s.BM25.load("animal_index_bm25", load_corpus=True)
    import bm25s
    import Stemmer  # optional: for stemming
    
    # Create your corpus here
    corpus = [
        "a cat is a feline and likes to purr",
        "a dog is the human's best friend and loves to play",
        "a bird is a beautiful animal that can fly",
        "a fish is a creature that lives in water and swims",
    ]
    
    # optional: create a stemmer
    stemmer = Stemmer.Stemmer("english")
    
    # Tokenize the corpus and only keep the ids (faster and saves memory)
    corpus_tokens = bm25s.tokenize(corpus, stopwords="en", stemmer=stemmer)
    
    # Create the BM25 model and index the corpus
    retriever = bm25s.BM25()
    retriever.index(corpus_tokens)
    
    # Query the corpus
    query = "does the fish purr like a cat?"
    query_tokens = bm25s.tokenize(query, stemmer=stemmer)
    
    # Get top-k results as a tuple of (doc ids, scores). Both are arrays of shape (n_queries, k).
    # To return docs instead of IDs, set the `corpus=corpus` parameter.
    results, scores = retriever.retrieve(query_tokens, k=2)
    
    for i in range(results.shape[1]):
        doc, score = results[0, i], scores[0, i]
        print(f"Rank {i+1} (score: {score:.2f}): {doc}")
    
    # You can save the arrays to a directory...
    retriever.save("animal_index_bm25")
    
    # You can save the corpus along with the model
    retriever.save("animal_index_bm25", corpus=corpus)
    
    # ...and load them when you need them
    import bm25s
    reloaded_retriever = bm25s.BM25.load("animal_index_bm25", load_corpus=True)
    # set load_corpus=False if you don't need the corpus"},{
  5. Configure Claude Desktop to use the bm25s MCP Server

    main

    To integrate the BM25 index with Claude Desktop, add a configuration entry to your claude_desktop_config.json. You must provide the absolute path to the uv executable and the absolute path to the directory containing bm25.

    {
      "mcpServers": {
        "bm25s": {
          "command": "/absolute/path/to/uv",
          "args": [
            "--directory",
            "/ABSOLUTE/PATH/TO/PARENT/FOLDER/bm25s",
            "run",
            "bm25",
            "mcp",
            "launch",
            "--index-dir",
            "/absolute/path/to/your/index"
          ]
        }
      }
    }
  6. Manage indices in the central user directory

    main

    You can use the -u flag to save indices to a central user directory (~/.bm25s/indices/). This allows you to search indices from anywhere without providing full file paths.

    To index to the central directory: bm25 index <file> -u -o <name>

    To search interactively from the central directory: bm25 search -u "<query>" (this will open a menu to select your index).

    # Save to the central directory using the -u flag
    bm25 index documents.csv -u -o my_docs
    
    # Search interactively!
    bm25 search -u "what is AI?"
  7. Use memory-mapped files for efficient large-scale retrieval

    main

    To handle large indices without exhausting RAM, use the mmap=True option when loading an index. This allows the index to be loaded as a memory-mapped file. Ensure you have saved your index using retriever.save() first.

    # Save the BM25 index to a file
    retriever.save("bm25s_very_big_index", corpus=corpus)
    
    # Load the BM25 index as a memory-mapped file
    retriever = bm25s.BM25.load("bm25s_very_big_index", mmap=True)
  8. Integrate with Hugging Face Hub

    main

    Use bm25s.hf.BM25HF to save and load BM25 indices directly from the Hugging Face Hub. This requires a HUGGING_FACE_HUB_TOKEN environment variable. You can save the index along with the corpus to the hub and reload it later, including options for memory-mapping and loading the corpus.

    import os
    import bm25s
    from bm25s.hf import BM25HF
    
    # Create a BM25 index
    retriever = BM25HF()
    # Create your corpus here
    corpus = [
        "a cat is a feline and likes to purr",
        "a dog is the human's best friend and loves to play",
        "a bird is a beautiful animal that can fly",
        "a fish is a creature that lives in water and swims",
    ]
    corpus_tokens = bm25s.tokenize(corpus)
    retriever.index(corpus_tokens)
    
    # Set your username and token
    user = "your-username"
    token = os.environ["HF_TOKEN"]
    retriever.save_to_hub(f"{user}/bm25s-animals", token=token, corpus=corpus)
    
    # --- Loading from Hub ---
    
    # Load a BM25 index from the Hugging Face model hub
    retriever = BM25HF.load_from_hub(f"{user}/bm25s-animals")
    
    # you can specify revision and load_corpus=True if needed
    retriever = BM25HF.load_from_hub(
        f"{user}/bm25s-animals", revision="main", load_corpus=True
    )
    
    # if you want a low-memory usage, you can load as memory map with `mmap=True`
    retriever = BM25HF.load_from_hub(
        f"{user}/bm25s-animals", load_corpus=True, mmap=True
    )
  9. Perform a 1-line search using the Python API

    main

    You can build a search engine over local files (.csv, .json, .jsonl, .txt) or lists of text using the BM25 module.

    1. Use BM25.load(path, document_column=...) to load documents. For CSV or JSONL, specify the column or key containing the text.
    2. Use BM25.index(corpus) to build the search index (handles tokenization and stemming).
    3. Use retriever.search(queries, k=...) to retrieve the top k results for a list of queries.
    import BM25
    
    # 1. Load your documents (supports .csv, .json, .jsonl, .txt)
    # For csv/jsonl, you can specify which column/key holds the text
    corpus = BM25.load("documents.csv", document_column="text")
    
    # 2. Build the search index
    retriever = BM25.index(corpus)
    
    # 3. Search!
    queries = ["how to learn python", "best search algorithms"]
    results = retriever.search(queries, k=5) # Get top 5 results
    
    # Print the top results for the first query
    for result in results[0]:
        print(f"Score: {result['score']:.2f} | Document: {result['document']}")
  10. Install bm25s

    main

    You can install bm25s using pip. It is highly recommended to install the [core] extra to include essential dependencies like JSON loading, progress bars, stemming, and JIT compilation via numba.

    # Basic installation
    pip install bm25s
    
    # Recommended installation (includes core dependencies)
    pip install "bm25s[core]"
    
    # Install only a stemmer for better results
    pip install PyStemmer
    
    # Install all extra dependencies (full suite)
    pip install "bm25s[full]"
    pip install "bm25s[core]"
  11. Use the bm25s MCP Server

    main

    The Model Context Protocol (MCP) server allows you to expose your BM25 index as a tool for LLMs and agents.

    Installation: Install with the mcp extra using uv:

    uv pip install "bm25s[mcp]"

    Launching: Use the bm25 CLI to launch the server:

    bm25 mcp launch --port 8000 --index-dir /path/to/your/index

    Available Tools:

    • retrieve(query: str, k: int = 10): Retrieves top-k documents.
    • get_info(): Returns index metadata (vocab size, doc count, backend).