Text search benchmarks
What a search box costs, and how good the answers are.
Text search
What a search box costs: match ranked by BM25, the marks
of what it matched, the facets beside it, and the words a typo or a half
typed word reaches. In one process on an M1.
| 100 000 documents of 30 words | median |
|---|---|
match … limit 10, two words | 0.06 ms |
… with highlight(), a row's marks | 1–3 µs more |
… with snippet(), a row's | 2–4 µs more |
| … with two facets over what it reaches, about 16 667 rows | 1.6 ms |
a facet over every row, through @hash | 0.01 ms |
| … read from every row | 6.1 ms |
fuzzy and prefix | |
|---|---|
| a word's neighbours among FiQA's 135 500 words, one edit | 14 µs |
| … two edits | 120 µs |
| … the words it begins | 4 µs |
the sorted words, made at the first loose match, 60 000 words | 6 ms, 1.3 MB |
- A query that asks for no marks runs as it did. The marks read the row's text again by the index's own tokenizer; nothing is kept for them in the index.
- How good the answers are is the next section: BM25 on two BEIR datasets, with a typo in each query and half typed, beside the vectors.
Method, and the commands that run it
100 000 documents of 30 words each over a vocabulary of 5 000, the
commoner words more common, a brand under @hash
(40 values) and a color with no index (12). Each figure is the
median of 15 runs over 200 two-word queries; a row's marks are the
difference over the ten rows of match alone. The words a typo
reaches are timed over FiQA's own words, 2 000 query words with a typo in
each, and the sorted words over 100 000 documents of made-up words, many an
edit or two from another, as a language's are (Typos and words begun, Highlights and snippets, Facets).
make search-bench
make fuzzy-bench BEIR=path/to/fiqaRetrieval quality
How good the answers are, not how fast: every way fenecdb ranks
documents, scored by nDCG@10 on two public BEIR datasets. Fusing keyword
and vector search (fuse) is the one path near the top of
both.
| SciFact | FiQA | FiQA p50 | |
|---|---|---|---|
match, BM25 | 0.662 | 0.232 | 0.26 ms |
near, the graph | 0.645 | 0.366 | 0.37 ms |
near … exact | 0.645 | 0.369 | 4.99 ms |
match … rerank, 50 candidates | 0.656 | 0.337 | 0.45 ms |
match … rerank, 200 (default) | 0.646 | 0.361 | 0.78 ms |
match … rerank, 1 000 | 0.649 | 0.369 | 1.82 ms |
match … near … fuse, 10 a side | 0.700 | 0.364 | 0.67 ms |
match … near … fuse, 20 (default) | 0.700 | 0.366 | 0.74 ms |
match … near … fuse, 100 | 0.688 | 0.359 | 0.97 ms |
match … near … fuse, 1 000 | 0.690 | 0.364 | 4.03 ms |
near, sparse (SPLADE++) | 0.693 | 0.331 | 0.68 ms |
match … near … fuse, sparse | 0.698 | 0.314 | 1.07 ms |
With a typo in each query, and with each query's last word
half typed, as a search box sends it: fuzzy and
prefix take back most of what is lost, and leave the queries
written whole where they were.
| SciFact | FiQA | FiQA p50 | |
|---|---|---|---|
match, as written | 0.662 | 0.232 | 0.26 ms |
match … fuzzy, as written | 0.665 | 0.233 | 0.74 ms |
match … prefix, as written | 0.663 | 0.234 | 0.32 ms |
match, a typo | 0.636 | 0.213 | 0.24 ms |
match … fuzzy, a typo | 0.663 | 0.229 | 0.71 ms |
match … fuzzy 1, a typo | 0.666 | 0.231 | 0.53 ms |
match, the last word half typed | 0.616 | 0.188 | 0.26 ms |
match … prefix, half typed | 0.655 | 0.208 | 0.60 ms |
match … fuzzy prefix, half typed | 0.658 | 0.210 | 1.09 ms |
How the typos were made
One typo a query, by a generator seeded by the query's place
(crates/fenec-bench/src/bin/beir.rs): in one word of four
letters or more, a letter dropped, a key beside it on the keyboard typed as
well or instead, or two letters swapped — the four kinds Damerau found four
misspellings in five to be — and in the first letter 7% of the time, as
Pollock and Zamora found it. SciFact's 300 test queries hold 297 such
words, FiQA's 648 hold 644. Half typed is the last word cut to half its
letters, rounded up. A word of under five letters takes no edit, which is
most of what fuzzy leaves unrecovered; with one typo a query,
fuzzy 1 does a little better than the two edits nine letters
allow, which pay where a long word holds two. The p50s are the whole
query's, the words found and their lists merged included.
Method, and what the BEIR runs found
cd crates/fenec-bench/beir && npm install
node embed.mjs path/to/scifact # dense: all-MiniLM-L6-v2, 90 MB
node splade.mjs path/to/scifact # sparse: SPLADE++, 532 MB, optional
make beir BEIR=path/to/scifactTwo BEIR datasets as BEIR ships them — SciFact, 5 183 documents and 300
test queries, and FiQA, 57 638 and 648 — with every document and query
embedded by all-MiniLM-L6-v2 the way sentence-transformers does it, and
encoded by SPLADE++ (prithivida/Splade_PP_en_v1) into a
sparse<30522> field, both cut at 256 tokens with the
closing [SEP] kept. transformers.js drops it when it cuts a
text, so both scripts cut texts themselves: for SPLADE that was worth 0.46
of SciFact's nDCG@10, for the dense vectors up to 0.003. Each of the
engine's ways of ranking ten documents is then scored by nDCG@10, through
FenecQL: the answers are the engine's own.
The two datasets pull opposite ways. In SciFact a claim shares its words
with the evidence and BM25 alone beats the transformer; in FiQA a question
shares few words with its answer and BM25 is far behind. fuse
is the one path near the top of both. The rank offset matters less than the
depth: at 20 a side, k from 10 to 120 moves either score by at
most 0.005.
SPLADE sits between the two. On SciFact it comes within 0.006 of the dense fusion with one index; on FiQA it lifts BM25 by 0.099 but stays under the dense vectors, and fused with BM25 there it loses 0.016 — both sides are lexical. Its queries' 37 to 65 terms reach most of the corpus, and its lists are summed a list at a time rather than walked a document at a time: 0.68 ms, where MaxScore's walk took 2.58 (How it works).