Autocomplete & prefix search in Whoosh

Three pure-Python approaches · by Priya Sundaram
TL;DR. "Search-as-you-type" is the single most-requested search feature, and Whoosh gives you three pure-Python ways to build it, each a good fit for a different situation: All the code below is copy-paste runnable on 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).

Tip. Terms are lowercased by the default analyzer, which is why the suggestions above are lowercase even though the titles are title-cased. That's usually what you want for matching; keep the original text in a 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…UseCost
Match documents by the start of a wordPrefix queryNone — no schema change
A dropdown of suggested wordsexpand_prefix()None — reads the term index
Match anywhere inside a word, or tolerate typosNGRAMWORDS fieldLarger 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.

Reminder about spelling correction. If your goal is really "did you mean…?" on full queries rather than as-you-type completion, Whoosh also ships a dedicated spell-checker (corrector) — see the documentation. N-grams and prefix queries are for completion; the corrector is for correction.
Whoosh is a pure-Python full-text search library — actively maintained again after two rounds of abandonment. If this helped you ship autocomplete without standing up a search server, a ⭐ on GitHub genuinely helps other people find a search library that's alive again.

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