I'm Priya Sundaram, the current maintainer of whoosh3 — the actively-maintained fork of the pure-Python Whoosh full-text search library. Every snippet below is verified against the current release (3.48.0).
You know the sidebar on every shopping site: Category (3), Brand (2), Price $0–100 (3) — click one and the list narrows, the counts update. That UI has a name: faceted search. It looks like the kind of thing you need Elasticsearch or Algolia for.
You don't. If your catalog is a few thousand rows, you can build the whole thing — ranked search and the counted facet sidebar and drill-down — with an embedded, pure-Python index. No server, no C extension, no API key. Here's how.
The data
from whoosh.fields import Schema, TEXT, KEYWORD, NUMERIC
from whoosh import index
schema = Schema(
name=TEXT(stored=True),
brand=KEYWORD(stored=True),
category=KEYWORD(stored=True),
price=NUMERIC(stored=True),
)
products = [
("Trail Running Shoe", "Terra", "shoes", 120),
("Road Running Shoe", "Terra", "shoes", 140),
("Waterproof Hiking Boot", "Summit", "shoes", 190),
("Merino Wool Socks", "Terra", "apparel", 22),
("Rain Jacket", "Summit", "apparel", 160),
("Down Jacket", "Aster", "apparel", 240),
("Trekking Poles", "Summit", "gear", 90),
("Headlamp", "Aster", "gear", 45),
]
import tempfile
ix = index.create_in(tempfile.mkdtemp(), schema)
w = ix.writer()
for name, brand, cat, price in products:
w.add_document(name=name, brand=brand, category=cat, price=price)
w.commit()
KEYWORD fields are exact, un-stemmed tokens — perfect for facet values like a brand or a category. NUMERIC lets us bucket prices.
The counted sidebar, in one call
The whole facet sidebar is a single groupedby= argument on the search:
from whoosh.query import Every
with ix.searcher() as s:
r = s.search(Every(), groupedby=["category", "brand"], limit=None)
print({k: len(v) for k, v in r.groups("category").items()})
print({k: len(v) for k, v in r.groups("brand").items()})
{'shoes': 3, 'apparel': 3, 'gear': 2}
{'Terra': 3, 'Summit': 3, 'Aster': 2}
r.groups("category") maps each facet value to the list of matching document IDs; len() gives you the count you paint next to the checkbox. That's your sidebar.
Price ranges, not just values
Nobody wants a checkbox per exact price. Bucket a numeric field with RangeFacet:
from whoosh import sorting
price_facet = sorting.RangeFacet("price", 0, 300, 100) # 0–100, 100–200, 200–300
with ix.searcher() as s:
r = s.search(Every(), groupedby={"price": price_facet}, limit=None)
print({str(k): len(v) for k, v in r.groups("price").items()})
{'(0, 100)': 3, '(100, 200)': 4, '(200, 300)': 1}
Facets follow the search, too
Facets aren't only for the full catalog — they recompute against whatever the user searched, exactly like a real store. Search the text field and group in the same call:
from whoosh.qparser import QueryParser
with ix.searcher() as s:
q = QueryParser("name", ix.schema).parse("shoe OR boot OR jacket")
r = s.search(q, groupedby="brand", limit=None)
print(len(r), "matches")
print({k: len(v) for k, v in r.groups("brand").items()})
5 matches
{'Terra': 2, 'Summit': 2, 'Aster': 1}
Five products match the query, and the brand counts describe those five — so the sidebar always reflects the current result set.
Drilling down
When the user clicks Brand: Summit, you don't re-run the text search from scratch — you add a cheap filter=:
from whoosh.query import Term
with ix.searcher() as s:
q = QueryParser("name", ix.schema).parse("shoe OR boot OR jacket")
r = s.search(q, filter=Term("brand", "Summit"), limit=None)
print([h["name"] for h in r])
['Waterproof Hiking Boot', 'Rain Jacket']
A filter query is cached separately from the scoring query, so repeated drill-downs on the same facet stay fast. Want the results sorted by price instead of relevance? Add sortedby="price".
When this is the right tool
This isn't a replacement for Elasticsearch at ten-million-document scale. It's the right-sized tool for the very common case in between a WHERE ... LIKE and a search cluster: a catalog, a docs site, an internal admin tool, a desktop app. You get ranking, facets, filters, and sorting from pip install whoosh3 and a file on disk — deployable anywhere Python runs, including read-only and serverless environments.
The index is embedded, the data never leaves your process, and the whole faceted UI above is maybe 30 lines of glue.
whoosh3 is the maintained fork of Whoosh (pure-Python, BM25, Apache-licensed). If you build something with it — or hit a rough edge — issues and PRs are genuinely welcome on GitHub. ⭐ helps others find it.
Top comments (0)