Explanation¶
The pipeline¶
A search string becomes a Q through three stages,
each owned by one module. The tokenizer classifies characters into a token
stream and records their source offsets, deferring every grammar decision to
the parser. The parser is a precedence-climbing (Pratt) parser: it consumes
the token stream and builds an AST, validating each field it meets against a
FieldRegistry as it goes, so an
unknown field or an unsupported operator raises
QueryParseError before compilation ever
starts. build_q() lowers that AST into a Django
Q with a single structural match – adding a new ORM mapping never
touches parsing, and adding new syntax never touches the ORM mapping.
status:open OR status:draft
"status:open OR status:draft"
-> tokenize() characters -> Token stream (ident, colon, term, or, ...)
-> parse() Pratt parser -> AST: Term | Field | Cmp | Range | Exists
| Not | And | Or
-> build_q() structural match -> Django Q
parse() and build_q are the two halves of that
middle-and-last stage; search_query_to_q()
chains tokenizing, parsing, and building for the common case in one call.
AND and OR are left-associative, OR binds loosest, and NOT/-/+
bind tightest as prefixes, so NOT a AND b parses as (NOT a) AND b –
juxtaposition (a b) is an implicit AND sharing its binding power, so
a b and a AND b parse identically.
Why two lexers exist¶
The tokenizer above is strict on purpose: it raises
QueryParseError on the first malformed
character, which is exactly what a query compiler should do – fail loud
rather than silently compile the wrong query. A live search box needs the
opposite guarantee: it has to colorize every keystroke, including a query
that is only half-typed – an unterminated quote, a dangling [.
For that, the package ships a second, presentation-only lexer,
highlight_query_spans(), that never
raises. It runs one regex over the
entire string, with a catch-all group absorbing any character it cannot
classify, so the span list it returns always covers the source end to end.
The two lexers share no code: keeping them independent means a change meant
to loosen highlighting can never accidentally loosen what the parser
accepts, and vice versa. It is also why ? can color as a wildcard in the
search box while the compiler still treats it as a literal character – see
Query language for that boundary case, and
Colored search input for the JavaScript port of
this same lexer that colors the admin search box client-side.
Lucene-inspired, not Lucene-compatible¶
The syntax borrows familiar Lucene shapes – field-scoped terms
(status:open), quoted phrases, AND/OR/NOT, grouping, comparisons,
and ranges – but the package does not claim full Lucene compatibility, and
some gaps are permanent rather than pending. There is no fuzzy match
(~), no boosting (^), and no proximity search. ? is not a
single-character wildcard, even though Lucene defines it as one: the
compiler only ever implements *. Treat the syntax as a small, predictable
subset of Lucene’s rather than a drop-in replacement for it.