FAQ and troubleshooting¶
Short, practical answers to the questions and error messages that come up most often. Every code snippet here is checked against the current release. If your problem isn’t covered, please open an issue or a discussion.
Installation¶
pip install whoosh gives me an ancient version¶
The name whoosh on PyPI still points at the original 2.7.4 release from
2016, which is unmaintained. The actively maintained fork is published as
whoosh3:
pip install whoosh3
The import name is unchanged — you still write import whoosh. Only the
distribution name differs, so existing code keeps working after you switch:
from whoosh.index import create_in
from whoosh.fields import Schema, TEXT, ID
If both are installed in the same environment, uninstall the old one first
(pip uninstall whoosh) so imports resolve to the maintained package.
Indexing¶
whoosh.store.LockError when I open a second writer¶
Whoosh allows only one writer at a time per index. This is by design: it is
how the on-disk format stays consistent without a server. Opening a second
ix.writer() while another is still open (not yet committed or cancelled)
raises LockError:
w1 = ix.writer()
w2 = ix.writer() # raises whoosh.store.LockError
Fixes, in rough order of preference:
Reuse one writer and batch your
add_documentcalls, thencommit()once. This is also much faster than a writer per document.If several threads or tasks in one process need to add documents, use
whoosh.writing.AsyncWriterorwhoosh.writing.BufferedWriter, which serialize writes for you. See the concurrency guide.If a previous run crashed and left a stale lock, make sure no other process holds the index, then remove the lock (
ix.storageexposes the lock; or delete the*.lockfile in the index directory when you are certain nothing else is writing).
Multiple readers/searchers are always fine — the single-writer rule only applies to writes.
My searcher doesn’t see documents I just added¶
A searcher is a point-in-time snapshot of the index. A searcher you opened before a commit will not see documents added by that commit — even in the same process:
s = ix.searcher()
w = ix.writer(); w.add_document(title="new"); w.commit()
s.search(qp.parse("new")) # 0 hits — snapshot is stale
s = ix.searcher() # open a fresh searcher...
s.search(qp.parse("new")) # 1 hit
# ...or refresh the one you have (cheaper than reopening from scratch):
s = s.refresh()
s.search(qp.parse("new")) # 1 hit
In a long-running service, reopen or refresh() your searcher after each
commit (or on a timer). refresh() reuses the parts of the index that didn’t
change, so it’s cheap to call often.
Nothing gets indexed / the index is empty¶
Almost always a missing commit. add_document buffers changes; they are
only written when you call commit():
w = ix.writer()
w.add_document(title="hello")
w.commit() # <- without this, nothing persists
If you use with ix.writer() as w: note that the writer commits on a clean
exit but cancels on an exception — so an error inside the block discards the
batch.
Searching¶
My search returns no results even though the word is there¶
The single most common cause is an analyzer mismatch between how the text
was indexed and how your query term is spelled. A TEXT field lowercases and
tokenizes its input, so the stored term for "Hello World" is hello and
world — lowercase. If you build a query term by hand with the original
casing, it won’t match:
from whoosh.query import Term
s.search(Term("body", "Hello")) # 0 hits — index stored "hello"
s.search(Term("body", "hello")) # 1 hit
The fix is to let the QueryParser run the field’s analyzer for you, so query terms are processed exactly like indexed terms:
from whoosh.qparser import QueryParser
qp = QueryParser("body", ix.schema) # pass the schema!
s.search(qp.parse("Hello")) # 1 hit — parser lowercases it
Passing ix.schema (not just the field name) is what makes the parser apply
the same analysis your fields use. This is why hand-built Term objects are a
frequent source of “no results”: they bypass analysis entirely.
Searching run doesn’t find running (or vice versa)¶
The default TEXT analyzer does not do stemming, so run and
running are different terms:
schema = Schema(body=TEXT) # no stemming
# query "run" does NOT match a document containing only "running"
To match across word inflections, index the field with a
StemmingAnalyzer:
from whoosh.analysis import StemmingAnalyzer
schema = Schema(body=TEXT(analyzer=StemmingAnalyzer()))
Whoosh’s built-in stemmer is deliberately conservative (it collapses common
suffixes, not every irregular form), but it makes inflected searches like
running → documents about running work as users expect. Because the
QueryParser uses the field’s analyzer, your queries are stemmed the same way
automatically. See the
stemming & folding guide.
ID and KEYWORD fields are case-sensitive and exact¶
ID fields are stored verbatim — not lowercased, not tokenized. That’s the
point (they hold things like paths, slugs, and UUIDs), but it means matches are
exact and case-sensitive:
schema = Schema(tag=ID)
# document indexed with tag="Fiction"
s.search(Term("tag", "Fiction")) # 1 hit
s.search(Term("tag", "fiction")) # 0 hits — case matters for ID
If you want case-insensitive keyword matching, use
KEYWORD(lowercase=True), or normalize the value yourself before indexing and
querying.
Special characters in my query raise a parse error¶
Characters like :, [, ], ~, *, (, ), and AND/OR
have meaning in the query language. If you are passing untrusted user input
and want it treated as literal words, either catch the parse error and fall
back, or restrict the parser’s plugins. A minimal parser with the fewest
surprises:
from whoosh.qparser import QueryParser, PlusMinusPlugin
qp = QueryParser("body", ix.schema)
qp.remove_plugin_class(...) # drop plugins you don't want to expose
See the query-language reference for the full grammar and which behaviours are opt-in plugins.
Range queries on numbers/dates return nothing¶
Numeric and date range queries only work when the field is a
NUMERIC or DATETIME field — a
number stored in a TEXT field is indexed as text and won’t compare
numerically. With the right field type, ranges work through the parser:
schema = Schema(price=NUMERIC(stored=True), name=TEXT)
qp = QueryParser("name", ix.schema)
qp.parse("price:[0 to 50]") # matches price 0..50 inclusive
For dates, add the DateParserPlugin so natural-language ranges parse. See the
dates guide.
Negative numbers lose their sign in a TEXT field¶
The default tokenizer used by every stock analyzer matches on \w+, which does
not include - or +. A leading sign is dropped, so in a TEXT field
-100 and 100 index to the same token:
from whoosh.analysis import RegexTokenizer
[t.text for t in RegexTokenizer()("balance -100 usd")] # -> ['balance', '100', 'usd']
This is expected for prose (you rarely want - glued onto words), but it
surprises people indexing prices, deltas or temperatures. Two fixes, depending on
what you need:
You want to filter or range on the number — store it in a
NUMERICfield (usesigned=Truefor negatives). ThenNumericRangeandprice:[lo to hi]compare numerically, which is what you almost always want for quantities:schema = Schema(id=ID(stored=True), delta=NUMERIC(signed=True, stored=True))
You genuinely want the signed token searchable as text — give the field a tokenizer whose pattern keeps the sign:
from whoosh.analysis import RegexTokenizer signed = RegexTokenizer(r"[-+]?\w+(\.?\w+)*") schema = Schema(note=TEXT(analyzer=signed)) [t.text for t in signed("balance -100 usd")] # -> ['balance', '-100', 'usd']
Still stuck?¶
Check the quickstart for an end-to-end example.
Try your query in the live browser demo to isolate whether it’s a schema/analyzer issue or something in your code.
Search existing issues, or open a new one with a small reproducer — a schema, one or two documents, and the query — and we’ll take a look.