Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To filter by a number or a date in Whoosh, declare the field as NUMERIC or DATETIME in the schema, then query it with NumericRange or DateRange. Both range objects include their endpoints unless you say otherwise. The behaviour described here comes from the official Whoosh 2.7.4 documentation. That version is the reference point for every API name and default below, so confirm it against the Whoosh and Python versions you actually run before you copy any code.

Choose the field type before you write the query

A range query only behaves correctly when the index stores the value in a typed form. Whoosh gives you two typed field types for this job:

  • NUMERIC stores integer or floating-point values. Whoosh converts each value into a sortable byte representation, which is what makes numeric comparisons possible. Pass Python numbers when you index.
  • DATETIME stores Python datetime.datetime objects. Pass datetime objects, not date strings, when you index.

If a value is stored as plain TEXT or ID, Whoosh compares it as a string. A price of 9 then sorts after 10, and a date written as 2024-06-01 does not get the chronological treatment a DATETIME field gives it. Decide the field type first; the query follows from it.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Filter numbers with NUMERIC and NumericRange

Declare the numeric field

from whoosh.fields import Schema, TEXT, NUMERIC, DATETIME, ID

schema = Schema(
    title=TEXT(stored=True),
    sku=ID(stored=True, unique=True),
    price=NUMERIC(stored=True),
    published=DATETIME(stored=True),
)

NUMERIC accepts configuration arguments that the fields documentation describes as bits, signed, decimal_places and shift_step. The shift_step setting controls tiered indexing. A lower shift step stores more index data in exchange for faster searches over wide ranges, and a value of zero turns tiered indexing off. The documentation presents this as a trade-off, not a measured speed-up, so test with your own data volume before tuning it. Argument names have shifted in older prose, so check them against the signature in your installed version.

Run the range query

from whoosh.query import NumericRange

# Inclusive on both ends: 10 <= price <= 50
q_inclusive = NumericRange("price", 10, 50)

# Exclusive on both ends: 10 < price < 50
q_exclusive = NumericRange("price", 10, 50, startexcl=True, endexcl=True)

# Open on one side is not expressed with None in this example;
# use a bound that fits your data (see the datetime section below).

The signature is NumericRange(fieldname, start, end, startexcl=False, endexcl=False, boost=1.0, constantscore=True). Two points matter in practice:

  • Pass numbers, not strings, for start and end. A string such as "10" is not the same input as the integer 10.
  • The default includes values equal to both boundaries. Set startexcl=True or endexcl=True to drop only the matching endpoint.

The API documentation also says constant-score matching can speed up typical filter use. That is a statement about how the query behaves, not a benchmark, and the documentation does not publish a figure for any workload.

Filter dates with DATETIME and DateRange

Index datetimes in UTC

The Whoosh date guide states that the indexer ignores the tzinfo attribute on a datetime. Attaching a timezone to a value therefore does not make the indexed value timezone-aware. The guide’s recommendation is the one quoted below:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“The best way to deal with time zones is to always index datetimes in native UTC form.”

Here, “native” means a naive datetime (no tzinfo) whose clock value is already in UTC. Convert local times before you pass them to the writer:

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

local = datetime(2024, 6, 1, 9, 30, tzinfo=ZoneInfo("Europe/London"))
utc_native = local.astimezone(timezone.utc).replace(tzinfo=None)

writer.add_document(title="Release notes", published=utc_native)

Apply the same conversion to query bounds. If you index in UTC and build your range bounds in local time, the endpoints will not line up with the stored values.

Build the date range

from datetime import datetime
from whoosh.query import DateRange

start = datetime(2024, 1, 1, 0, 0)
end = datetime(2024, 12, 31, 23, 59, 59)

q = DateRange("published", start, end)  # inclusive by default
q_excl = DateRange("published", start, end, startexcl=True, endexcl=True)

DateRange is a thin subclass of NumericRange. It converts datetime endpoints to numbers and otherwise behaves the same way, so the inclusion flags and the inclusive default carry over. Choose the end bound with care: a date-only value such as datetime(2024, 12, 31) is midnight, so it excludes anything stored later that day.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handle open-ended ranges

The date guide says DATETIME fields do not currently support open-ended ranges. Its documented workaround is to use an endpoint far in the past or future. Pick a bound that sits outside your valid data, for example:

from datetime import datetime
from whoosh.query import DateRange

# "Everything on or after 2020-01-01", with a far-future upper bound
q = DateRange("published", datetime(2020, 1, 1), datetime(2100, 1, 1))

This is a workaround for the documented version, not a general rule. If your data could contain dates after 2100, widen the bound.

Query-string ranges: brackets, braces and their limits

When users type a query string, the default query language decides which endpoints are included:

  • [apple TO bear] includes both endpoints.
  • {apple TO bear} excludes both endpoints.
  • Mixed delimiters work, so [apple TO bear} includes the start and excludes the end.

These forms are term ranges. They compare strings in lexical order, and this is the part readers most often misread. A date written as date:[20050101 TO 20090715] works because the stored strings sort in the same order as the dates. A numeric range written as price:[9 TO 10] is compared as text, so the result will not match what a numeric comparison gives. For numbers and datetimes, the documented route is the typed NumericRange or DateRange object built in your code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The optional GtLtPlugin adds comparison syntax such as field:>apple or date:>='31 march 2001', which Whoosh translates into ranges. It is a parser plugin you must add explicitly, so do not assume it is present in every parser you build.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Parse human-readable dates with DateParserPlugin

If users type dates in ordinary language, the DateParserPlugin can translate them. The date guide’s examples include forms such as date:2005 and date:20050624, and the plugin’s free=True option lets unquoted date text follow the field prefix. Keep these limits in mind:

  • The Whoosh 2.7.4 guide describes the plugin as experimental.
  • It parses English date expressions only.
  • Relative expressions, such as “next Friday”, depend on a base datetime. Set that base deliberately, or the same query will resolve to different dates on different days.

For production search where the input format is controlled, prefer the typed range objects. They are simpler to test and do not depend on an experimental parser.

Choose the right approach

Choice Use it when Main consideration
NumericRange The indexed field is NUMERIC Pass numbers. Endpoints are inclusive unless startexcl or endexcl is set.
DateRange The indexed field is DATETIME Pass datetime objects. Index and query in native UTC. Open-ended bounds need a far-off substitute.
Query-string brackets [ ] A user types a range and the field is text or a lexically sortable form Inclusive. Compares strings, so numbers can sort incorrectly.
Query-string braces { } The user needs exclusive endpoints on a lexical range Exclusive. Same lexical caveat as brackets.
GtLtPlugin Users write comparisons such as >= Optional plugin; must be added to the parser. Translates to ranges.
DateParserPlugin Users type natural-language dates Experimental in the 2.7.4 guide, English only, and relative terms depend on a base datetime.

Confirm your version before you rely on the examples

The documentation used here is for Whoosh 2.7.4, and it does not establish compatibility with current Python releases. Run these checks before you copy the code into a project:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Check the installed library version: pip show whoosh. Compare the reported version with 2.7.4.
  2. Check your interpreter: python --version. Run the examples under the same version your application uses.
  3. Index two or three known records, including one on each side of every boundary, and run each range query. Confirm that the endpoint records behave as the inclusion flags predict.
  4. Repeat the timezone test with a record just before and just after midnight UTC, so you can see whether your conversion holds.

Troubleshooting common results

  • A numeric range returns records outside the bounds. The field is probably TEXT or ID, or the query uses bracket syntax on numbers. Declare the field as NUMERIC and build the query with NumericRange.
  • A record on the boundary date is missing or extra. Check whether you intended an inclusive end. A bound of datetime(2024, 12, 31) is midnight, so it excludes the rest of that day.
  • Results shift by several hours. The stored value was probably local time, or the bound was built in local time while the index holds UTC. Convert both sides to native UTC.
  • An open-ended date query returns nothing. The placeholder bound is outside your data. Widen the far-past or far-future value.
  • A parsed natural-language date changes from day to day. The relative base datetime is not fixed. Pass an explicit base instead of relying on the current time.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.