Whoosh vs SQLite FTS5: which should you use for full-text search in Python?

by Priya Sundaram · 14 July 2026
Short answer: If you already have data in SQLite and just need fast keyword matching, use FTS5 — it's a mature C extension that ships with Python's sqlite3 module and is very fast and compact. Reach for Whoosh when you want a real search engine you can program in pure Python: custom analyzers and stemming, pluggable BM25/TF-IDF scoring, faceting, result highlighting, "did you mean?" spelling correction, and an index you can run anywhere CPython runs — including PyPy and the browser via WebAssembly — with no C toolchain and no separate service.

Adding search to a Python app almost always comes down to one of these two, once you've decided you don't want to stand up Elasticsearch or a hosted service. Both are embeddable, both run in-process, and both are free. They make very different trade-offs, though, and picking the wrong one leads to either a painful rewrite or a search box that never quite does what your users expect. This is a practical guide to choosing, with real numbers.

The core difference in one sentence

SQLite FTS5 is a full-text index bolted onto a C database engine. Whoosh is a full-text search engine written entirely in Python. That single fact explains almost every trade-off below.

Feature-by-feature comparison

 WhooshSQLite FTS5
ImplementationPure Python, zero runtime depsC extension (bundled with CPython's sqlite3)
Installpip install whoosh3Built in — no install
Raw speedGood; slower than CVery fast
Index sizeCompactCompact
Relevance rankingBM25F (default), TF-IDF, or your own scoringBM25 (bm25() aux function)
Stemming / analysisPluggable analyzers, stemming, stop words, n-grams, custom filtersFixed tokenizers (unicode61, porter, trigram); limited customisation
Query languageRich, extensible parser (fields, ranges, fuzzy, boosts, plugins)FTS5 MATCH syntax (AND/OR/NOT, phrase, NEAR, prefix)
HighlightingBuilt-in, multiple formatters/fragmentershighlight()/snippet() aux functions
Faceting / groupingBuilt-in facets and sortingDo it yourself with SQL
Spelling / "did you mean?"Built-in, off the indexNot built in
Runs on PyPy / WASM / no-compile envsYesDepends on the SQLite build available
Programmability from PythonEverything is a Python object you can subclassYou drive it through SQL strings

Benchmarks: how much speed are you trading away?

Here's the honest part. SQLite FTS5 is a heavily optimised C extension, and it is much faster than pure Python. The repo ships a reproducible micro-benchmark (examples/benchmark_vs_sqlite.py) that indexes a synthetic corpus and times build, size, and query latency. On a modest CI-class machine, 20,000 documents / 300 queries:

MetricWhooshSQLite FTS5
Index build7.6 s0.16 s
Index size10.1 MB10.2 MB
Avg query latency~43 ms~0.04 ms

Read that plainly: FTS5 builds and queries orders of magnitude faster, and index sizes are about the same. If you are searching millions of documents on a hot path, that gap matters and you should probably use FTS5 (or something even bigger). Whoosh's sweet spot is thousands to low-millions of documents where a ~40 ms query is completely fine, and where the things it gives you — pluggable analysis, real ranking control, faceting, highlighting, spelling — save you more engineering time than the raw speed costs you. Numbers vary by machine and corpus; reproduce with your own data before deciding.

The same task in both

Whoosh

from whoosh.fields import Schema, TEXT, ID
from whoosh.analysis import StemmingAnalyzer
from whoosh.filedb.filestore import RamStorage
from whoosh.qparser import QueryParser

schema = Schema(path=ID(stored=True),
                body=TEXT(analyzer=StemmingAnalyzer()))
ix = RamStorage().create_index(schema)
w = ix.writer()
w.add_document(path="/a", body="Indexing large document collections")
w.commit()

with ix.searcher() as s:
    q = QueryParser("body", ix.schema).parse("index")   # stemmed: matches "Indexing"
    print([hit["path"] for hit in s.search(q)])   # ['/a']

SQLite FTS5

import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE VIRTUAL TABLE docs USING fts5(path, body, tokenize='porter')")
db.execute("INSERT INTO docs VALUES (?, ?)", ("/a", "Indexing large document collections"))
rows = db.execute("SELECT path FROM docs WHERE docs MATCH 'index'").fetchall()
print([r[0] for r in rows])   # [('/a',)] -> ['/a']

Both work. The difference shows up the moment you need something FTS5 doesn't offer out of the box: a custom analyzer for your domain (product SKUs, code identifiers, CJK text), a "did you mean?" suggestion, faceted navigation, or a scoring tweak. In Whoosh those are objects you configure or subclass; in FTS5 you're either writing SQL gymnastics or reaching for another tool.

A decision checklist

Pick SQLite FTS5 if…

Pick Whoosh if…

And it isn't strictly either/or: some apps keep records in SQLite and build a Whoosh index for the search experience, using each where it's strongest.

Whoosh is maintained again. It's pure Python, zero runtime dependencies, BSD-2-Clause, with a live browser demo and modern packaging.

⭐ Star Whoosh on GitHub  ·  Try the live demo  ·  pip install whoosh3

See also: Whoosh vs Tantivy (tantivy-py).