- Prefix queries — match documents whose words start with what the user has typed. Zero schema changes.
- Term completion with
expand_prefix()— produce a ranked list of suggestions (the words themselves) to show under the search box. - N-gram fields — match anywhere inside a word (true substring / "contains" search), tolerant of typos, at the cost of a larger index.
pip install whoosh3 and was executed against the current release
to capture the output verbatim.
Every example uses the same tiny in-memory index so you can paste it straight
into a REPL. In a real app you'd point Whoosh at a directory on disk with
index.create_in("indexdir", schema) instead of
RamStorage — everything else is identical.
from whoosh.fields import Schema, TEXT, ID
from whoosh.filedb.filestore import RamStorage
schema = Schema(id=ID(stored=True), name=TEXT(stored=True))
ix = RamStorage().create_index(schema)
writer = ix.writer()
for i, title in enumerate([
"The Matrix", "Matrix Reloaded", "Interstellar", "Inception",
"The Imitation Game", "Interview with the Vampire", "Interstate 60",
]):
writer.add_document(id=str(i), name=title)
writer.commit()
1. Prefix queries — "words that start with…"
The simplest approach needs no schema changes at all. Whoosh has a
built-in Prefix query that matches any document containing a term
beginning with your string. This is exactly what you want when the user has
typed a partial word and you want to return matching documents.
from whoosh import query
with ix.searcher() as s:
q = query.Prefix("name", "inte")
for hit in s.search(q, limit=None):
print(hit["name"])
Interstellar
Interview with the Vampire
Interstate 60
You can also let the query parser build prefix queries from a trailing
*, e.g. QueryParser("name", schema).parse("inte*").
Prefix queries are cheap and index-free, which makes them the right default
for autocomplete over short fields like titles, tags, or usernames.
2. Term completion — suggest the words themselves
Sometimes you don't want to return documents; you want to show a dropdown of
completions ("did you mean interstellar?"). For that,
ask the reader for every indexed term that starts with the prefix using
expand_prefix():
with ix.searcher() as s:
reader = s.reader()
suggestions = [
term.decode() if isinstance(term, bytes) else term
for term in reader.expand_prefix("name", "inte")
]
print(suggestions)
['interstate', 'interstellar', 'interview']
These come back in sorted term order, straight from the index's term
dictionary, so it's fast even on large indexes. A common pattern is to feed
each suggestion back into a Prefix or Term query to
fetch the top documents once the user picks one. If you want completions
ranked by how often a term appears, sort the suggestions by
reader.frequency("name", term).
stored=True field (like name here) so you
can display it with its original casing.
3. N-gram fields — match inside a word (substring & typos)
Prefix search only matches the start of a word. If a user types
trix expecting to find Matrix, or
makes a small typo, you need N-grams: Whoosh's
NGRAMWORDS field type splits each word into overlapping character
chunks so that any chunk of the query can match anywhere inside a word.
from whoosh.fields import Schema, TEXT, ID, NGRAMWORDS
from whoosh.qparser import QueryParser
schema = Schema(
id=ID(stored=True),
name=TEXT(stored=True),
grams=NGRAMWORDS(minsize=2, maxsize=5, stored=False), # the searchable copy
)
ix = RamStorage().create_index(schema)
writer = ix.writer()
for i, title in enumerate([
"The Matrix", "Matrix Reloaded", "Interstellar", "Inception",
"The Imitation Game", "Interview with the Vampire", "Interstate 60",
]):
writer.add_document(id=str(i), name=title, grams=title) # index into both
writer.commit()
with ix.searcher() as s:
qp = QueryParser("grams", schema)
for term in ["inte", "trix", "cept"]:
hits = s.search(qp.parse(term), limit=None)
print(term, "->", [h["name"] for h in hits])
inte -> ['Interstate 60', 'Interstellar', 'Interview with the Vampire']
trix -> ['The Matrix', 'Matrix Reloaded']
cept -> ['Inception']
Notice trix matched Matrix and cept matched
Inception — matches from the middle of a word, which prefix
search can't do. The same mechanism also makes N-gram search naturally
tolerant of minor misspellings, since a wrong letter only breaks the grams it
touches. The trade-off is index size: a field of 2–5 character grams is
several times larger than a plain TEXT field, so keep the range
tight (minsize=2, maxsize=5 is a good starting point) and index
grams only for the fields that need "contains" search.
Which one should I use?
| You want… | Use | Cost |
|---|---|---|
| Match documents by the start of a word | Prefix query | None — no schema change |
| A dropdown of suggested words | expand_prefix() | None — reads the term index |
| Match anywhere inside a word, or tolerate typos | NGRAMWORDS field | Larger index |
In practice, many apps combine them: an NGRAMWORDS field for the
forgiving as-you-type box, plus a normal TEXT field with BM25
ranking for the "real" search once the user hits enter. Because Whoosh is a
single pure-Python library, all of this lives in one process with no extra
service to run.
corrector) — see the
documentation. N-grams and prefix queries are for
completion; the corrector is for correction.
Install from PyPI · Python quickstart · Try the live demo
See also: Search files from the command line · Full-text search in Flask · Whoosh vs SQLite FTS5