autobm25 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. autobm25-0.1.0/.gitignore +49 -0
  2. autobm25-0.1.0/LICENSE +21 -0
  3. autobm25-0.1.0/MANIFEST.in +7 -0
  4. autobm25-0.1.0/PKG-INFO +325 -0
  5. autobm25-0.1.0/README.md +295 -0
  6. autobm25-0.1.0/README_CN.md +243 -0
  7. autobm25-0.1.0/autobm25/__init__.py +187 -0
  8. autobm25-0.1.0/autobm25/__main__.py +7 -0
  9. autobm25-0.1.0/autobm25/_accel.py +113 -0
  10. autobm25-0.1.0/autobm25/bm25_engine.py +282 -0
  11. autobm25-0.1.0/autobm25/cli.py +191 -0
  12. autobm25-0.1.0/autobm25/data/config.yaml +57 -0
  13. autobm25-0.1.0/autobm25/data/param_dictionary.json +2037 -0
  14. autobm25-0.1.0/autobm25/data/stopwords.txt +175 -0
  15. autobm25-0.1.0/autobm25/demo.py +190 -0
  16. autobm25-0.1.0/autobm25/dictionary.py +170 -0
  17. autobm25-0.1.0/autobm25/feature_extractor.py +228 -0
  18. autobm25-0.1.0/autobm25/loader.py +168 -0
  19. autobm25-0.1.0/autobm25/metrics.py +68 -0
  20. autobm25-0.1.0/autobm25/rule_predictor.py +111 -0
  21. autobm25-0.1.0/autobm25.egg-info/PKG-INFO +325 -0
  22. autobm25-0.1.0/autobm25.egg-info/SOURCES.txt +49 -0
  23. autobm25-0.1.0/autobm25.egg-info/dependency_links.txt +1 -0
  24. autobm25-0.1.0/autobm25.egg-info/entry_points.txt +2 -0
  25. autobm25-0.1.0/autobm25.egg-info/requires.txt +8 -0
  26. autobm25-0.1.0/autobm25.egg-info/top_level.txt +1 -0
  27. autobm25-0.1.0/config.yaml +56 -0
  28. autobm25-0.1.0/logo/logo.png +0 -0
  29. autobm25-0.1.0/pyproject.toml +72 -0
  30. autobm25-0.1.0/requirements.txt +4 -0
  31. autobm25-0.1.0/rust/Cargo.lock +180 -0
  32. autobm25-0.1.0/rust/Cargo.toml +39 -0
  33. autobm25-0.1.0/rust/README.md +62 -0
  34. autobm25-0.1.0/rust/build_pyext.py +129 -0
  35. autobm25-0.1.0/rust/src/corpus.rs +219 -0
  36. autobm25-0.1.0/rust/src/engine.rs +201 -0
  37. autobm25-0.1.0/rust/src/features.rs +240 -0
  38. autobm25-0.1.0/rust/src/fx.rs +69 -0
  39. autobm25-0.1.0/rust/src/grid.rs +307 -0
  40. autobm25-0.1.0/rust/src/json.rs +614 -0
  41. autobm25-0.1.0/rust/src/lib.rs +28 -0
  42. autobm25-0.1.0/rust/src/loader.rs +364 -0
  43. autobm25-0.1.0/rust/src/main.rs +707 -0
  44. autobm25-0.1.0/rust/src/metrics.rs +202 -0
  45. autobm25-0.1.0/rust/src/params.rs +742 -0
  46. autobm25-0.1.0/rust/src/pyext.rs +422 -0
  47. autobm25-0.1.0/rust/src/stats.rs +171 -0
  48. autobm25-0.1.0/rust/src/text.rs +146 -0
  49. autobm25-0.1.0/rust/tests/core.rs +139 -0
  50. autobm25-0.1.0/setup.cfg +4 -0
  51. autobm25-0.1.0/setup.py +194 -0
@@ -0,0 +1,49 @@
1
+ # ---------------------------------------------------------------------------
2
+ # Local-only material. Everything below stays on the machine that produced it:
3
+ # the corpora, the experiments behind the shipped dictionary, and the scratch
4
+ # tooling. Only the package itself (autobm25/, rust/, setup.py, READMEs) is
5
+ # meant to reach a clone or a wheel.
6
+ # ---------------------------------------------------------------------------
7
+
8
+ # Datasets (BEIR downloads, subsamples, language splits) - fetched, never shipped
9
+ dataset/
10
+ dataset_en/
11
+ dataset_cn/
12
+ datasets/
13
+
14
+ # Research pipeline output: benchmark tables, grid searches, mixes, documents
15
+ results/
16
+ research/
17
+ document/
18
+
19
+ # Local tooling and scratch directories (bench harnesses, downloaded installers)
20
+ _bench/
21
+ _tools/
22
+ _scratch/
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Python build artefacts
26
+ # ---------------------------------------------------------------------------
27
+ __pycache__/
28
+ *.py[cod]
29
+ *.egg-info/
30
+ build/
31
+ dist/
32
+ release/
33
+ .venv/
34
+ venv/
35
+ .pytest_cache/
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Rust build output and the compiled Python extension.
39
+ #
40
+ # `rust/` itself is deliberately *tracked*: the sources are what let a user
41
+ # compile the accelerator on their own machine (`python rust/build_pyext.py`).
42
+ # Only the artefacts are ignored - `rust/target/` is cargo's scratch directory
43
+ # and the compiled extension is rebuilt by `pip install .` / `pip wheel .`
44
+ # (see setup.py), so a stale binary can never be committed by accident.
45
+ # ---------------------------------------------------------------------------
46
+ rust/target/
47
+ *.pyd
48
+ *.so
49
+ *.dylib
autobm25-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tzy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ # Ship the Rust sources in the sdist so `pip install autobm25` from an sdist can
2
+ # still compile the extension when a toolchain is available. Built artifacts and
3
+ # the cargo target directory stay out of the archive.
4
+ include LICENSE README.md README_CN.md
5
+ recursive-include rust *.rs Cargo.toml Cargo.lock build_pyext.py README.md
6
+ prune rust/target
7
+ global-exclude *.pyd *.so *.dylib
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.1
2
+ Name: autobm25
3
+ Version: 0.1.0
4
+ Summary: Zero-label BM25 hyperparameter adaptation: O(1) statistical-feature dictionary lookup with heuristic fallback
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/SaturnAura/AutoBM25
7
+ Project-URL: Repository, https://github.com/SaturnAura/AutoBM25
8
+ Project-URL: Issues, https://github.com/SaturnAura/AutoBM25/issues
9
+ Project-URL: Changelog, https://github.com/SaturnAura/AutoBM25/releases
10
+ Keywords: bm25,information-retrieval,hyperparameter,zero-shot,ir
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Rust
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Classifier: Topic :: Text Processing :: Indexing
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ Provides-Extra: test
29
+ License-File: LICENSE
30
+
31
+ # AutoBM25 — Zero-Label, Zero-Tuning BM25 Hyperparameter Adaptation
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/autobm25.svg)](https://pypi.org/project/autobm25/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/autobm25.svg)](https://pypi.org/project/autobm25/)
35
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/SaturnAura/AutoBM25/blob/main/LICENSE)
36
+ [![Rust MSRV](https://img.shields.io/badge/rust-1.63%2B-orange.svg)](https://github.com/SaturnAura/AutoBM25/blob/main/rust/Cargo.toml)
37
+ [![CI](https://github.com/SaturnAura/AutoBM25/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SaturnAura/AutoBM25/actions/workflows/ci.yml)
38
+
39
+ > [Chinese README](https://github.com/SaturnAura/AutoBM25/blob/main/README_CN.md)
40
+
41
+ ![AutoBM25 logo](https://raw.githubusercontent.com/SaturnAura/AutoBM25/main/logo/logo.png)
42
+
43
+ > Feed the corpus's **statistical features** into an **O(1) parameter dictionary** and get the optimal BM25 hyperparameters (k1 / b / k3 / δ / IDF) directly; fall back to interpretable heuristics when nothing matches. Verified on 16 BEIR datasets: **16/16 never worse than the BM25 defaults, NDCG@10 +18.0% median / +30.5% mean, 92.0% leave-one-corpus-out dictionary hit rate**.
44
+
45
+ > 😄 Maybe we should call it **BM26**? — if the hyperparameters can adapt, so can the version number (just kidding, BM25 is BM25).
46
+
47
+ ---
48
+
49
+ ## Why
50
+
51
+ BM25 is the most widely used ranking model in information retrieval, but its **optimal hyperparameters vary dramatically across corpora** — short texts, long texts, code, legal documents, Q&A communities each need different k1/b values. In practice, everyone uses the fixed defaults `k1=1.2, b=0.75`, because tuning requires a labeled validation set, and **new corpora rarely have labels**.
52
+
53
+ AutoBM25 takes a different path: **optimal parameters are a function of the corpus's statistical structure, and statistical structure can be measured without any labels.** Measure 21 normalized dictionary features, look up the parameters — zero labels, zero tuning, constant time.
54
+
55
+ ## Highlights
56
+
57
+ - 🗂️ **21 dictionary features** (15 corpus + 6 query-side) across length distribution, term-frequency redundancy, vocabulary growth, and query statistics — each with an explicit mechanism hypothesis
58
+ - ⚡ **O(1) parameter dictionary**: nearest-neighbor lookup from feature vectors to optimal parameters, with **out-of-distribution (OOD) guard** and **heuristic fallback**; 48 entries ship with the repo (16 benchmark corpora + 32 cross-domain blends), each validated to be no worse than the defaults on the corpus it came from
59
+ - 🔧 **k3 query-term-frequency saturation**: ablation-verified, up to **+19.9%** on long-query scenarios
60
+ - 🧩 **Corpus mixing**: combine existing corpora into cross-domain blends to fill feature-space gaps; dictionary hit rate **70.6% → 92.0%**
61
+ - 🚀 **Scales to 8.84M documents** (msmarco); self-built numpy-vectorized BM25/BM25+ engine with streaming stratified subsampling that keeps all relevant annotations
62
+ - 🦀 **Rust/PyO3 acceleration**: the tokenize-and-index stage is available as a compiled extension, **5.2–12.4× faster one-pass end-to-end** on the benchmark below, with identical scores (optional; pure Python still works)
63
+
64
+ ## Results (16 BEIR datasets)
65
+
66
+ **BM25 defaults vs AutoBM25 (adaptive) — MRR@10 / NDCG@10 / Recall@100**
67
+
68
+ Each cell shows `BM25 defaults → AutoBM25` on the same corpus, queries and relevance
69
+ judgements (defaults: k1=1.2, b=0.75, k3=0, δ=0, rsj IDF). All 16 datasets improve;
70
+ none is worse than the default it is compared with.
71
+
72
+ | Dataset | MRR@10 | NDCG@10 | Recall@100 |
73
+ | ---------------- | ------------------: | ------------------: | ------------------: |
74
+ | arguana | 0.2036 → 0.2411 | 0.3158 → 0.3654 | 0.9090 → 0.9573 |
75
+ | climate-fever | 0.2480 → 0.3049 | 0.1778 → 0.2241 | 0.4551 → 0.5306 |
76
+ | dbpedia-entity | 0.6674 → 0.6919 | 0.3863 → 0.4158 | 0.4779 → 0.5152 |
77
+ | fever | 0.5983 → 0.8467 | 0.6105 → 0.8322 | 0.9007 → 0.9536 |
78
+ | fiqa | 0.1727 → 0.2050 | 0.1387 → 0.1624 | 0.3429 → 0.3799 |
79
+ | hotpotqa | 0.8131 → 0.8376 | 0.6478 → 0.6803 | 0.7700 → 0.7830 |
80
+ | msmarco | 0.4024 → 0.4162 | 0.4400 → 0.4548 | 0.7490 → 0.7530 |
81
+ | nfcorpus | 0.4600 → 0.4740 | 0.2633 → 0.2738 | 0.1983 → 0.2027 |
82
+ | nq | 0.1594 → 0.1957 | 0.1774 → 0.2266 | 0.5937 → 0.6587 |
83
+ | scidocs | 0.2228 → 0.2596 | 0.1215 → 0.1444 | 0.2936 → 0.3260 |
84
+ | scifact | 0.5033 → 0.5574 | 0.5388 → 0.5945 | 0.8116 → 0.8334 |
85
+ | trec-covid | 0.5936 → 0.7832 | 0.3322 → 0.5011 | 0.0573 → 0.0764 |
86
+ | trec-covid-beir | 0.6107 → 0.7982 | 0.3394 → 0.5113 | 0.0575 → 0.0761 |
87
+ | trec-covid-v2 | 0.4079 → 0.7941 | 0.2127 → 0.5041 | 0.0495 → 0.0933 |
88
+ | vihealthqa | 0.3824 → 0.3921 | 0.3676 → 0.3758 | 0.6247 → 0.6289 |
89
+ | webis-touche2020 | 0.4931 → 0.7170 | 0.2226 → 0.3901 | 0.4336 → 0.5250 |
90
+ | **Average** | **0.4337 → 0.5322** | **0.3308 → 0.4160** | **0.4828 → 0.5183** |
91
+
92
+ Relative NDCG@10 gain over the 16 corpora: **+18.0% median, +30.5% mean** (worst
93
+ +2.2%, best +137.0%).
94
+
95
+ How those numbers are produced, because it matters when reading them:
96
+
97
+ - Each dictionary entry is selected by evaluating a candidate pool on the **full**
98
+ query set of its source corpus, and the BM25 defaults are always in that pool. An
99
+ entry therefore cannot be worse than the defaults on the corpus it was built from,
100
+ and the table measures the shipped prediction path end to end (dictionary lookup,
101
+ heuristic fallback on a miss).
102
+ - Five corpora (dbpedia-entity, climate-fever, fever, hotpotqa, msmarco) are
103
+ subsampled to 200k–300k documents. The sampler keeps **every** annotated relevant
104
+ document, so absolute values are not comparable with full-corpus BEIR leaderboards;
105
+ both numbers in a cell come from the same sample, queries and judgements.
106
+ - For a corpus that is *not* in the dictionary, parameters come from the nearest
107
+ entry or the heuristics. Leaving the target's own entry out, the dictionary still
108
+ answers **92.0%** of the 50 targets (the 16 corpora plus 34 cross-domain blends).
109
+
110
+ ## Quick Start
111
+
112
+ ```bash
113
+ pip install autobm25
114
+ ```
115
+
116
+ For a source checkout, use `pip install .`. If Cargo is available, the installer
117
+ tries to build the optional Rust extension; otherwise it installs the fully
118
+ functional pure-Python backend.
119
+
120
+ Then hand it a corpus - a plain list of strings is all it takes, there is no file format to learn:
121
+
122
+ ```python
123
+ from autobm25 import AutoBM25
124
+
125
+ corpus = [
126
+ "BM25 is a probabilistic ranking function used in information retrieval.",
127
+ "Automatic parameter tuning improves retrieval effectiveness.",
128
+ ]
129
+ retriever = AutoBM25()
130
+ retriever.index(corpus) # a plain list of strings is enough — no files needed
131
+ print(retriever.params) # hyperparameters chosen for this corpus
132
+ retriever.search("parameter tuning", top_k=5)
133
+ ```
134
+
135
+ `corpus` can be anything list-shaped: strings, `{"id", "text"}` records, a column of
136
+ a dataframe, documents you fetched from an API. Nothing is written to disk and no
137
+ directory layout is required. Pass `queries=[...]` to `index()` as well and the
138
+ query-side statistics (query length, query/term overlap, idf) join the same feature
139
+ vector.
140
+
141
+ Documents already in files? `autobm25 --dataset dataset/XXX` runs the same pipeline
142
+ over a directory, and `--interactive` turns it into a prompt - the CLI is a
143
+ convenience wrapper over this API, not a requirement. `autobm25 --demo` needs no files
144
+ at all.
145
+
146
+ The CLI prints the hyperparameters chosen for the dataset (for example,
147
+ `k1=0.5 b=0.495 k3=0.7 δ=0.97 idf=smoothed`).
148
+
149
+ The parameter dictionary is bundled inside the package (`autobm25/data/param_dictionary.json`), so retrieval works out of the box after installation — no need to run any experiments first.
150
+
151
+ Want to see it run before wiring up your own data? `python -m autobm25 --demo` (or the
152
+ `autobm25 --demo` console script) indexes a small built-in corpus - ten invented
153
+ documents about brewing, bicycles and retrieval - and prints the hyperparameters it
154
+ chose for that corpus together with the ranking for a handful of queries, with no
155
+ dataset directory involved. `autobm25 --demo --interactive` then drops you into a
156
+ prompt where you can type your own queries against the same corpus, and
157
+ `examples/quickstart.py` is the same thing as a copy-paste script over a made-up
158
+ support-desk corpus.
159
+
160
+ ## Rust acceleration (PyO3)
161
+
162
+ The retrieval core is also implemented in Rust and exposed as a **PyO3** extension
163
+ module, so the hot loops run as compiled code while the API, the feature
164
+ definitions and the rankings stay exactly as documented above.
165
+
166
+ The extension is optional. Published platform wheels include it, while a source
167
+ install compiles it when Cargo is available and falls back to the pure-Python
168
+ implementation when it is not. `autobm25.BACKEND` reports which one is live, and
169
+ `AUTOBM25_NO_RUST=1` forces the Python path.
170
+
171
+ ### Which stages are covered
172
+
173
+ The whole pipeline is: load -> tokenize + index -> features -> predict -> search.
174
+ The extension covers the three middle stages plus scoring; the two Python stages are
175
+ the ones where Python is simply the right tool.
176
+
177
+ | Stage | Implementation | Cost |
178
+ | ----------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------- |
179
+ | loading documents and queries | Python | I/O + `json` parsing, seconds to tens of seconds on multi-GB files; this is why the loader is *not* in Rust |
180
+ | tokenize + inverted index (`build_index`) | **Rust** | one pass over the corpus; also collects the feature statistics on the way |
181
+ | feature vector (`extract_features`) | **Rust** | read off that same pass instead of re-tokenizing |
182
+ | parameter prediction (dictionary lookup + heuristics) | Python | O(1): 48 entries x 21 features, **0.1-0.5 ms per corpus**, cached config/dictionary |
183
+ | `search` / `search_many` | **Rust** | posting-list accumulation, GIL released, one scorer per worker thread |
184
+ | evaluation metrics (CLI batch mode) | Python | numpy over the returned rankings, milliseconds |
185
+
186
+ The loader stays in Python because it is I/O plus JSON parsing, and on the multi-GB
187
+ corpora it is the dominant wall-clock cost - a Rust rewrite would not change the
188
+ disk reads. Prediction stays in Python for a different reason: it is a constant-time
189
+ lookup (0.1-0.5 ms against a 3 s index build), so moving it would buy nothing.
190
+
191
+ ### What the Rust extension replaces
192
+
193
+ The bottleneck is not scoring — that part is already vectorized numpy. It is a Python
194
+ loop over every token in the corpus with a dictionary operation per token, **run
195
+ twice**: once to collect the feature statistics and once to build the inverted index.
196
+
197
+ | Stage | Python | Rust |
198
+ | ------------------------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
199
+ | tokenize + term counting | `str.lower().split()` plus a dict operation per token | one pass over the corpus; terms live in a flat table, so only the first occurrence of a term hashes its string |
200
+ | feature statistics | re-tokenizes every document, and keeps a per-document dict for **all** of them in memory | read off the index build — the corpus is tokenized **once**, not twice |
201
+ | inverted index | one `np.asarray` per distinct term over Python-built posting lists | postings are built during that same pass |
202
+ | BM25 scoring | vectorized, but `np.add.at` scatter-add plus a full-array sort per query | posting-list accumulation, no per-query allocation, GIL released, one scorer per worker thread |
203
+
204
+ `index()` therefore tokenizes the corpus once instead of twice, which is the single
205
+ largest change. The pure-Python `BM25Engine` is unchanged and still available.
206
+
207
+ ### Do end users need Rust? No.
208
+
209
+ PyO3 is a **build-time** tool: the Rust code is compiled once into an ordinary
210
+ Python extension module, and the compiled result is what ships. Anyone who
211
+ installs the package just imports it — there is no compiler, no toolchain and no
212
+ extra step on their side:
213
+
214
+ ```python
215
+ import autobm25 # picks up the compiled extension if present
216
+
217
+ retriever = autobm25.AutoBM25() # Rust-backed
218
+ retriever.index(corpus) # one tokenizing pass
219
+ retriever.search("parameter tuning", top_k=5)
220
+
221
+ print(autobm25.BACKEND) # "rust"
222
+ ```
223
+
224
+ That is what a wheel gives you: `pip install <wheel>` (or `pip install autobm25` from
225
+ an index, where pip picks the wheel matching the OS/CPU) and nothing else, because the
226
+ extension is already inside. `abi3-py39` is what makes the matrix manageable - one
227
+ binary per **platform** serves CPython 3.9 and every later version, so wheels are
228
+ built per OS/architecture (`win_amd64`, `manylinux_x86_64`, `macosx_arm64`, …) rather
229
+ than per Python version. A single universal wheel is not possible: a compiled
230
+ extension is platform-specific by definition, which is also why a source checkout
231
+ with no compiled extension present builds a `py3-none-any` pure-Python wheel.
232
+
233
+ Nothing here is ever fatal. `autobm25/_accel.py` imports the extension when it is
234
+ there and falls back to the Python engine when it is not, so `import autobm25`
235
+ cannot fail because of a missing or mismatched binary — it only runs ~7× slower.
236
+ `AUTOBM25_NO_RUST=1` forces the Python backend at runtime,
237
+ `AUTOBM25_SKIP_RUST_BUILD=1` skips the compile step during an install, and
238
+ `autobm25.backend_info()` explains which backend is live.
239
+
240
+ From a source checkout it comes down to `cargo`: with a toolchain on PATH,
241
+ `pip install .` compiles the extension for you, and `python rust/build_pyext.py`
242
+ does the same thing by hand (`--check` also verifies that it imports). Without a
243
+ toolchain you still get a fully working pure-Python package. Set
244
+ `AUTOBM25_REQUIRE_RUST=1` for release builds that must fail rather than silently
245
+ omit the accelerator. Toolchains are tried in order - the default first, then any
246
+ installed `*-windows-gnu` one - and `--toolchain <name>` pins one explicitly.
247
+
248
+ ### Measured speedup
249
+
250
+ Measured on 2026-11-09 on Windows with 12 logical cores, CPython 3.11.5,
251
+ numpy 1.26.4 and scipy 1.12.0. Same machine, same data and process,
252
+ `top_k=100`; the Python column is the same package with `AUTOBM25_NO_RUST=1`.
253
+
254
+ | Dataset | Docs | Tokens | Queries | Build index + features | Search (all queries) | End-to-end |
255
+ | ------------- | ------: | -----: | ------: | --------------------------: | --------------------------: | --------------------------: |
256
+ | arguana | 8,674 | 1.45M | 1,406 | 2.27 s → 0.24 s (**9.6×**) | 3.87 s → 0.22 s (**17.6×**) | 6.38 s → 0.52 s (**12.4×**) |
257
+ | scifact | 5,183 | 1.11M | 1,109 | 1.16 s → 0.13 s (**8.8×**) | 0.39 s → 0.06 s (**6.2×**) | 1.59 s → 0.23 s (**6.8×**) |
258
+ | nfcorpus | 3,633 | 0.85M | 3,237 | 0.97 s → 0.11 s (**9.3×**) | 0.35 s → 0.07 s (**4.9×**) | 1.50 s → 0.29 s (**5.2×**) |
259
+ | trec-covid-v2 | 129,179 | 27.2M | 50 | 41.27 s → 4.22 s (**9.8×**) | 0.37 s → 0.05 s (**7.1×**) | 42.88 s → 5.26 s (**8.1×**) |
260
+
261
+ *End-to-end* = load + parameter prediction + index + features + search. The split
262
+ path (`extract_features` and `build_index` called separately, each tokenizing the
263
+ corpus) is **3.7–8.9×** faster.
264
+
265
+ Reading the table: the gap generally widens with corpus size because Python's
266
+ per-token dictionary work is linear in tokens. Scoring gains most on long-query
267
+ workloads (arguana, 17.6×) where the old `np.add.at` scatter-add is slowest, and
268
+ the batch path uses every core with the GIL released.
269
+
270
+ ### Parity
271
+
272
+ Running both backends over the same corpus and diffing the feature vector, the chosen
273
+ hyperparameters, the full ranked list of every query and MRR@10 / NDCG@10 /
274
+ Recall@100 gives the same answer either way. On scifact, nfcorpus, arguana and
275
+ trec-covid-v2:
276
+
277
+ - feature values agree to about `1e-11` — they come from different floating-point summation
278
+ orders over identical numbers — and the chosen hyperparameters are identical;
279
+ - every ranked list is bitwise identical, except where documents **tie** at the
280
+ `top_k` cutoff: `np.argpartition` returns an arbitrary member of a tie group,
281
+ while Rust applies the tie-break the Python code documents (lowest document index
282
+ wins). Scores stay bitwise equal, MRR@10 and NDCG@10 are unaffected, and
283
+ Recall@100 can move in the 4th decimal when the tied document at rank 100 is
284
+ judged relevant in one list but not the other.
285
+
286
+ ## Limitations
287
+
288
+ - **Out-of-distribution (OOD)**: an entry is validated on the corpus it was built
289
+ from, not on yours. With the target's own entry removed, the dictionary still
290
+ answers 75% of the 16 corpora above — but it is then predicting from a *different*
291
+ corpus, and that is not free: in this leave-one-corpus-out setting 12/16 improve
292
+ (mean +17.0%, median +7.7%) while 4 get worse than the defaults (-8% to -23%:
293
+ fiqa, arguana, climate-fever, vihealthqa — three of which miss the dictionary
294
+ entirely and land on the heuristics). The OOD guard and the heuristic fallback
295
+ limit the damage; they do not remove it. If your corpus is not represented in the
296
+ dictionary, validate the returned parameters before trusting them.
297
+ - **Non-English corpora**: a few features are language-dependent (English stopword
298
+ density, stopword-filtered term statistics). Vietnamese vihealthqa is in the
299
+ dictionary and lands at +2.2%, but for a language the dictionary has never seen,
300
+ validate the returned parameters before trusting them.
301
+
302
+ ## Project Structure
303
+
304
+ ```
305
+ autobm25/
306
+ ├── autobm25/ # the pip package: retrieval API (engine, features, dictionary, CLI) + data/
307
+ ├── rust/ # Rust core + PyO3 bindings (python rust/build_pyext.py)
308
+ ├── tests/ # pytest API, loader, metrics, dictionary and parity tests
309
+ ├── .github/workflows/ # three-platform CI and Trusted Publishing
310
+ ├── examples/ # runnable library examples (quickstart.py, ...)
311
+ ├── logo/
312
+ ├── config.yaml # heuristic coefficients
313
+ ├── setup.py # compiles the extension into the wheel (metadata is in pyproject.toml)
314
+ ├── pyproject.toml # pip packaging
315
+ ├── requirements.txt
316
+ └── README.md / README_CN.md
317
+ ```
318
+
319
+ ## License
320
+
321
+ [MIT License](LICENSE): the code can be freely **copied, modified, and used commercially**; the only requirement is to **retain the copyright notice and this license text** in copies (i.e., attribution). Linking the repository when using the project satisfies this requirement.
322
+
323
+ ---
324
+
325
+ [Chinese README](https://github.com/SaturnAura/AutoBM25/blob/main/README_CN.md)