API reference¶
The public surface is small: one call for the common case,
parse()/build_q() when you
want the AST in between, a field registry, and one exception. The optional
admin integration adds a single mixin. See Query language for the syntax these
compile, and the Tutorial to run them end to end.
Core¶
django-search-query. search_query_to_q() is the one
call most sites need; it chains parse() (string to
AST) and build_q() (AST to
Q). Call the two directly only when you want to
inspect or transform the AST before compiling.
-
django_search_query.search_query_to_q(query, *, registry, field_map, default_fields)¶django_search_query.search_query_to_q(query, *, registry, field_map, default_fields)¶
Parse
queryand compile it to a DjangoQin one call.- Parameters:
query (
str) – The user-supplied search string.registry (
FieldRegistry) – Field schema used to validate field names.field_map (
Mapping[str,str]) – Maps canonical field names to ORM lookup paths.default_fields (
Sequence[str]) – ORM paths searched for bare terms and phrases.
- Returns:
The compiled query.
- Return type:
Q- Raises:
QueryParseError– Ifquerycannot be parsed.
Examples
>>> from django_search_query.registry import FieldRegistry, FieldSpec >>> reg = FieldRegistry(specs=(FieldSpec(name="status", kind="enum"),)) >>> search_query_to_q( ... "status:open", registry=reg, field_map={"status": "status"}, ... default_fields=("title",), ... ) <Q: (AND: ('status__iexact', 'open'))>
-
django_search_query.parse(query, registry)¶django_search_query.parse(query, registry)¶
Parse
queryinto an AST, validating fields againstregistry.- Parameters:
query (
str) – The user-supplied search string.registry (
FieldRegistry) – Field schema; unknown fields raise a positioned error.
- Returns:
Root of the parsed tree. Single terms return a bare
Term(no one-childAnd/Orwrapper).- Return type:
Node- Raises:
QueryParseError– On any lexical or grammatical failure, with aposition.
Examples
>>> from django_search_query.registry import FieldRegistry, FieldSpec >>> reg = FieldRegistry(specs=(FieldSpec(name="status", kind="enum"),)) >>> node = parse("hello world", reg) >>> type(node).__name__, len(node.children) ('And', 2) >>> parse("status:open", reg) Field(field='status', value='open', kind='enum')
-
django_search_query.build_q(node, field_map, *, default_fields)¶django_search_query.build_q(node, field_map, *, default_fields)¶
Compile
nodeinto a DjangoQobject.- Parameters:
- Returns:
The compiled query, ready for
queryset.filter.- Return type:
Q
Examples
>>> from django_search_query.ast import Term >>> build_q(Term(value="hi"), {}, default_fields=("title",)) <Q: (AND: ('title__icontains', 'hi'))> >>> from django_search_query.ast import Field >>> build_q(Field("status", "open", "enum"), {"status": "state"}, ... default_fields=()) <Q: (AND: ('state__iexact', 'open'))>
-
class django_search_query.registry.FieldRegistry¶class django_search_query.registry.FieldRegistry¶
Examples
>>> registry = FieldRegistry( ... specs=( ... FieldSpec(name="status", kind="enum"), ... FieldSpec(name="author", kind="string", aliases=("by",)), ... ), ... ) >>> registry.get("by").name 'author' >>> registry.get("missing") is None True >>> registry.known_names() ('status', 'author')
-
class django_search_query.registry.FieldSpec¶class django_search_query.registry.FieldSpec¶
Bases:
objectSchema entry describing one queryable field.
- Parameters:
name (
str) – Canonical, user-facing field name (the token left of:).kind (
FieldKind) – Value kind, controlling how the compiler lowersfield:value.path (
str) – ORM lookup path the field resolves to; defaults toname.enum_values (
tuple[str, ...]) – Allowed values whenkind == "enum"(informational for v1).aliases (
tuple[str, ...]) – Alternate names that resolve to this spec.supports_comparison (
bool) – Whether>,<,>=,<=are accepted.supports_range (
bool) – Whether[a TO b]/{a TO b}ranges are accepted.
-
class django_search_query.errors.QueryParseError¶class django_search_query.errors.QueryParseError¶
Bases:
ValueErrorRaised when a query string cannot be tokenized or parsed.
Carries the byte offset into the original query so callers can render a caret-style pointer at the offending character.
Examples
>>> from django_search_query.errors import QueryParseError >>> err = QueryParseError("unexpected token", position=4) >>> err.position 4 >>> isinstance(err, ValueError) True
Highlighting¶
A second, presentation-only lexer colorizes a query for a live search box.
Unlike the parser it never raises – it tokenizes even a half-typed query so
every keystroke can be highlighted – which is why it is a separate function
rather than a mode of parse(). See
the pipeline for why the two
lexers stay independent, and
colored-input for the
JavaScript port that colors the admin search box.
-
django_search_query.highlight.highlight_query_spans(query)¶django_search_query.highlight.highlight_query_spans(query)¶
Lex
queryinto contiguous(start, role, text)highlight spans.Never raises: malformed input (an unterminated quote, a lone bracket) still yields a full, gap-free span list so a live editor can colorize an in-progress query character-for-character.
Examples
>>> highlight_query_spans("status:open") [Span(start=0, role='field', text='status'), Span(start=6, role='punct', text=':'), Span(start=7, role='value', text='open')] >>> [s.role for s in highlight_query_spans("a OR b")] ['value', 'whitespace', 'keyword', 'whitespace', 'value'] >>> [s.role for s in highlight_query_spans('-draft "half')] ['negation', 'value', 'whitespace', 'phrase'] >>> "".join(s.text for s in highlight_query_spans("created:>2024-01-01")) 'created:>2024-01-01'
-
django_search_query.highlight.apply_registry_errors(spans, registry)¶django_search_query.highlight.apply_registry_errors(spans, registry)¶
Return
spanswith registry-rejected fields and values re-rolederror.A second pass over the lexer output that the plain lexer cannot do alone: it consults
registryto flag an unknown field (both thefieldspan and its value are re-rolederror) and an out-of-enum value (only thevaluespan). Spans the registry accepts are returned unchanged.- Parameters:
spans (
Sequence[Span]) – Output ofhighlight_query_spans().registry (
FieldRegistry) – Field schema the values are validated against.
- Returns:
A new list; inputs are never mutated.
- Return type:
Examples
>>> from django_search_query.registry import FieldRegistry, FieldSpec >>> reg = FieldRegistry( ... specs=(FieldSpec(name="status", kind="enum", ... enum_values=("open", "draft")),), ... ) >>> [s.role for s in apply_registry_errors( ... highlight_query_spans("status:bogus"), reg)] ['field', 'punct', 'error'] >>> [s.role for s in apply_registry_errors( ... highlight_query_spans("author:tony"), reg)] ['error', 'punct', 'error']
-
class django_search_query.highlight.Span¶class django_search_query.highlight.Span¶
Bases:
NamedTupleOne contiguous highlight span with its source offset.
- Parameters:
start (
int) – Zero-based offset of the span in the source query.role (
str) – One ofHIGHLIGHT_ROLES.text (
str) – The exact source substring the span covers.
-
Every role
highlight_query_spans()andapply_registry_errors()emit.erroris never produced by the plain lexer; onlyapply_registry_errors()promotes a span to it.
Admin¶
django-admin-search-query (optional).
SearchQueryAdminMixin is the one class
this package ships: mount it ahead of
ModelAdmin (see the
admin how-to) and it wires
the query language into
get_search_results(), adds two JSON
endpoints behind the colored input, and injects the URLs those endpoints need
into the changelist template. Everything below is optional configuration on top
of that one mixin – most sites only ever set the three class attributes from
the how-to guide.
-
class django_admin_search_query.mixin.SearchQueryAdminMixin¶class django_admin_search_query.mixin.SearchQueryAdminMixin¶
Bases:
objectParse the changelist search box with the structured query language.
Subclasses configure the language through the three class attributes below, or override the matching
get_*hooks for dynamic behavior.
Hook contracts¶
Each class attribute has a matching get_* method, so dynamic behavior – a
registry that depends on request.user, say – only needs to override the
method, not chase every place the attribute is read. Called with no overrides,
the hooks read the class attributes straight through, and
get_search_query_default_fields()
falls back to search_fields (stripped
of Django’s lookup sigils – ^, =, @, $) whenever
search_query_default_fields
is left empty:
>>> from django_admin_search_query.mixin import SearchQueryAdminMixin
>>> class ExampleAdmin(SearchQueryAdminMixin):
... search_fields = ("^title", "=slug")
>>> ExampleAdmin().get_search_query_default_fields()
('title', 'slug')
get_search_results()
is the method Django’s changelist actually calls. It strips the search term and
returns super().get_search_results(...) unchanged – Django’s own
search_fields behavior – whenever the term is blank, no registry is
configured, or QueryParseError is raised
while compiling it; otherwise it returns a filtered
QuerySet: queryset.filter(q), False. That second
element is Django’s may_have_duplicates flag; it is False for the
local-column field maps this integration targets, where filtering by
Q adds no joins. A field map that instead points at
a to-many relation can introduce duplicate rows – collapse them with
.distinct() in that case.
JSON endpoints¶
get_urls() prepends
two model-scoped endpoints to the admin’s URLs, each wrapped in
admin_site.admin_view so it inherits the admin’s staff-login gate, and each
additionally checking
has_view_permission() before responding:
search-tokens/(named<app>_<model>_search_tokens) – the registry’s fields, kinds, operators, enum values, and field aliases, plus the default fields, asJsonResponse.search-highlight/(named<app>_<model>_search_highlight) – the highlight spans for a?q=query string, asJsonResponse.
Both endpoints are always registered once the mixin is mounted. The changelist
template only advertises them – as data-* attributes the colored input
reads – when
search_query_registry
is configured; without a registry, the search box renders as stock admin with
no extra markup. See
colored-input for the
exact JSON shape of each endpoint and how the JavaScript input consumes it.