Skip to main content

A tiny, zero-dependency BM25-lite in-memory text search index.

Project description

zerosearch

A tiny, zero-dependency BM25-lite in-memory text search index — standard library only, a single small module, and good enough to power retrieval for a RAG pipeline. Designed to run anywhere Python runs, including constrained environments like Cloudflare Python Workers (Pyodide) where pulling in scikit-learn/numpy is not an option.

It is a spiritual cousin of minsearch: same Index / .fit() / .search() API, but reimplemented from scratch with no third-party dependencies.

Drop-in replacement

zerosearch mirrors the minsearch API — Index(text_fields, keyword_fields), index.fit(docs), and index.search(query, filter_dict, boost_dict, num_results) — so you can swap it in without changing your call sites. It is used exactly this way in DataTalksClub/faq-assistant as the retrieval engine.

Note on ranking vs minsearch: zerosearch uses BM25-lite scoring, not minsearch's TF-IDF + cosine similarity — different algorithms, so the rankings are not bit-for-bit identical. Retrieval quality is on par, though: on the faq-assistant benchmark zerosearch matches minsearch's recall (it surfaces the same relevant documents in the top results), it just orders them differently. It is 100% identical to the in-repo BM25-lite engine it replaced.

Install

pip install zerosearch

Usage

from zerosearch import Index

docs = [
    {"id": "1", "title": "Docker compose basics", "text": "how to start services", "course": "de"},
    {"id": "2", "title": "Kafka consumers", "text": "consumer groups explained", "course": "de"},
]

index = Index(
    text_fields=["title", "text"],
    keyword_fields=["id", "course"],
)
index.fit(docs)

results = index.search(
    "how do I start docker compose",
    filter_dict={"course": "de"},             # keyword filter (scalar = exact, list = IN)
    boost_dict={"title": 3.0, "text": 1.0},   # per-field boosts
    num_results=5,
)
for result in results:
    print(result["score"], result["title"])

Each result is a shallow copy of the original document dict with an added "score" key.

Filtering

A filter_dict value can be a scalar (exact match) or a list/tuple/set (match any of the values — IN / OR within that field). Filters on different fields are combined with AND.

index.search("docker", filter_dict={"course": "de"})            # course == "de"
index.search("docker", filter_dict={"course": ["de", ""]})      # course == "de" OR course == ""
index.search("docker", filter_dict={"course": ["de", "mlops"], "kind": "faq"})  # (de OR mlops) AND faq

Saving & loading a prebuilt index

fit() does all the tokenization work up front. For latency-sensitive or cold-start-sensitive deployments (serverless functions, CLIs) you can build the index once — e.g. in CI — and ship the prebuilt artifact, so the process loads in milliseconds instead of re-tokenizing the whole corpus on startup.

# build step (CI / offline)
Index(text_fields=["title", "text"], keyword_fields=["id", "course"]).fit(docs).save("index.zsx")

# runtime (loads in ~ms, no re-tokenization)
index = Index.load("index.zsx")
results = index.search("docker compose")

dumps() / loads() are the in-memory equivalents (return/accept bytes). The artifact is a marshal blob, so documents must hold only marshal-able values (the JSON-like types a search corpus normally contains). Loading verifies a format tag and the platform's array item sizes and raises ValueError on a mismatch — rebuild from source if the format version or Python/platform changed. If you built with a custom tokenizer, pass it back in: Index.load("index.zsx", tokenizer=my_tokenizer) (the default tokenizer plus its stop words restore automatically).

How it works

  • Packed runtime statefit() builds with Counter scaffolding, then compacts the index into flat array buffers (a CSR-style postings list). That packed form is what search() reads and what save()/load() round-trip, so a prebuilt index loads without rebuilding any Python objects per document.

  • Tokenizer — lowercased word/number tokens; keeps + . # _ - inside a token so c++, node.js, f-string survive (a token must start with a letter/digit). Drops 1-character tokens and a small English stop-word list (both overridable).

  • Inverted index — built once in fit(). A query only scores documents that actually contain a query term, so search is fast even on large corpora.

  • Ranking — BM25-lite: each query term contributes boost * idf * (term_frequency / sqrt(field_length)) per field. IDF and document frequencies are computed over the filtered candidate set.

Customizing

index = Index(
    text_fields=["title", "text"],
    stop_words={"the", "a", "an"},          # replace the default stop words
    tokenizer=lambda s: s.lower().split(),  # or plug in your own tokenizer
)
index.fit(docs)

License

WTFPL.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zerosearch-0.3.0.tar.gz (62.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zerosearch-0.3.0-py3-none-any.whl (9.4 kB view details)

Uploaded Python 3

File details

Details for the file zerosearch-0.3.0.tar.gz.

File metadata

  • Download URL: zerosearch-0.3.0.tar.gz
  • Upload date:
  • Size: 62.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for zerosearch-0.3.0.tar.gz
Algorithm Hash digest
SHA256 67026b01562edbebb44bc6ad788d967572ad6eec7120041beec122fd1e3b85c6
MD5 f2edd2ae946159938f7253d6019d0151
BLAKE2b-256 b9ab964a7e2bd5e88b5a0230bb19234223feabfe020d2953792919e3c21f4d0b

See more details on using hashes here.

File details

Details for the file zerosearch-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: zerosearch-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 9.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for zerosearch-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5b9a3dc4acbbd4578ee2c6f1d35cb51e34b7bb6891b9084f11523767ea7781d2
MD5 0466ab459adc72dcee3c86c40bb27781
BLAKE2b-256 d66109464c703e88d56e2c8b6ff0c810c4e612b411d93431ae4ec930befb449d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page