How-to guides¶
Task recipes for tuning search_query_to_q()
beyond the tutorial’s defaults: which ORM path a field name resolves to,
which fields a query may touch at all, how wildcards and ranges compile, and
how to degrade gracefully when a query does not parse.
Map field names to ORM paths¶
field_map decouples the field name a user types from the ORM lookup path
the compiler builds. A registered field defaults to its own name when
field_map leaves it out, so most sites only add an entry for a field whose
ORM path really differs – an alias, or a lookup that crosses a relation with
Django’s __ traversal.
>>> from django_search_query import search_query_to_q
>>> from django_search_query.registry import FieldRegistry, FieldSpec
>>> registry = FieldRegistry(specs=(FieldSpec(name="author", kind="string"),))
>>> search_query_to_q(
... "author:tony",
... registry=registry,
... field_map={"author": "author"},
... default_fields=("title", "body"),
... )
<Q: (AND: ('author__icontains', 'tony'))>
field_map={} would compile identically here, since author’s ORM path
already matches its name.
Restrict which fields a query can touch¶
A FieldRegistry is a strict allow
list: only names declared as a
FieldSpec are queryable. A term for
anything else – a stray internal field, a typo – raises
QueryParseError at parse time instead of
silently matching nothing.
>>> from django_search_query import search_query_to_q
>>> from django_search_query.registry import FieldRegistry, FieldSpec
>>> registry = FieldRegistry(
... specs=(
... FieldSpec(name="title", kind="string"),
... FieldSpec(name="status", kind="enum"),
... ),
... )
>>> search_query_to_q(
... "status:open",
... registry=registry,
... field_map={},
... default_fields=("title",),
... )
<Q: (AND: ('status__iexact', 'open'))>
status is declared, so it compiles; a query for a field the registry never
listed raises – see Catch parse errors and fall back for how to catch that.
Wildcards and ranges¶
String fields support two wildcards: a trailing * anchors the match to the
start of the value (istartswith), a leading * anchors it to the end
(iendswith); a bare value, or one wrapped in * on both sides, stays
icontains. A date or number field whose
FieldSpec opts in also accepts
ordered comparisons (>, >=, <, <=) and inclusive or exclusive ranges
([a TO b] / {a TO b}).
>>> from django_search_query import search_query_to_q
>>> from django_search_query.registry import FieldRegistry, FieldSpec
>>> registry = FieldRegistry(specs=(FieldSpec(name="title", kind="string"),))
>>> search_query_to_q(
... "title:report*",
... registry=registry,
... field_map={},
... default_fields=(),
... )
<Q: (AND: ('title__istartswith', 'report'))>
>>> from django_search_query import search_query_to_q
>>> from django_search_query.registry import FieldRegistry, FieldSpec
>>> registry = FieldRegistry(
... specs=(FieldSpec(name="created", kind="date", supports_range=True),),
... )
>>> search_query_to_q(
... "created:[2024-01-01 TO 2024-12-31]",
... registry=registry,
... field_map={},
... default_fields=(),
... )
<Q: (AND: ('created__gte', '2024-01-01'), ('created__lte', '2024-12-31'))>
? looks like it should be a single-character wildcard, Lucene-style – it
is not. The presentation lexer that drives syntax highlighting recognizes
? as a wildcard-role character (see Explanation), but the
compiler only ever implements *. A literal ? in a value passes straight
through to icontains:
>>> from django_search_query import search_query_to_q
>>> from django_search_query.registry import FieldRegistry, FieldSpec
>>> registry = FieldRegistry(specs=(FieldSpec(name="title", kind="string"),))
>>> search_query_to_q(
... "title:a?c",
... registry=registry,
... field_map={},
... default_fields=(),
... )
<Q: (AND: ('title__icontains', 'a?c'))>
A bare value on a date or number field is not parsed into a date or
number either – without a comparison or range operator it takes the same
icontains path as a string field:
>>> from django_search_query import search_query_to_q
>>> from django_search_query.registry import FieldRegistry, FieldSpec
>>> registry = FieldRegistry(specs=(FieldSpec(name="created", kind="date"),))
>>> search_query_to_q(
... "created:2024",
... registry=registry,
... field_map={},
... default_fields=(),
... )
<Q: (AND: ('created__icontains', '2024'))>
Catch parse errors and fall back¶
Any malformed query – an unknown field, an unsupported operator, an
unterminated quote – raises
QueryParseError with the character offset
of the failure. A search endpoint that always needs to return something
can catch it and fall back to a plain substring search:
>>> from django_search_query import search_query_to_q
>>> from django_search_query.errors import QueryParseError
>>> from django_search_query.registry import FieldRegistry, FieldSpec
>>> registry = FieldRegistry(specs=(FieldSpec(name="status", kind="enum"),))
>>> try:
... search_query_to_q(
... "bogus:1", registry=registry, field_map={}, default_fields=("title",),
... )
... except QueryParseError as err:
... print(f"fell back: {err}")
fell back: unknown field 'bogus'; known fields: status
err.position is the offset into the original string – handy for pointing
a caret at the exact character that broke.
Colored search input uses the same lexer output
to color a live search box.