Skip to main content

stapel-search

CI coverage pypi downloads python license llms.txt

A materialized search index with one swappable engine seam: full-text with per-language configs and a typo-tolerant second arm, drill-down facet counts with a benchmark-calibrated candidate cap and an approximate fallback, indexed range filters over a numeric side table, geo radius and bounding-box search including the antimeridian, five sorts with an opaque keyset cursor whose envelope matches AnchorPagination, a registry-generated P2B Art. 5 ranking disclosure with a mandatory promoted marker on every result, and an event-to-pull indexer carrying the four Projection guarantees (idempotency, ordering, rebuild, drift_check). Ships three engines behind the same conformance suite: Postgres FTS (default, zero new infrastructure), Meilisearch (the [meili] extra) and a declared naive backend for SQLite demos.

Part of the Stapel framework — composable Django apps that deploy as a monolith or as microservices without changing module code.

Install

pip install stapel-search

At a glance

Fact Value
Version 0.14.7
Python >=3.11 (3.11, 3.12, 3.13)
Django djangorestframework>=3.14
HTTP operations 5
Config axes 8
Usage surface 41
Extension points 6
Error codes 55
Fleet dependencies stapel-attributes · stapel-categories (optional) · stapel-core · stapel-geo · stapel-listings (optional)

Documentation

OpenAPI · capabilities.json · llms.txt (for agents)

What this is

stapel-search is the fleet's search module: a materialized index table it owns, one dotted-path backend seam, and a hard rule that every indexed field has a source, a read path and a test.

It knows nothing about listings. Documents arrive through an open source registry that starts empty, and the composite that is allowed to know both the corpus and the index declares the entry. That is why the same module indexes a catalogue, a chat archive or a profile directory without a fork.

Why the index is its own table

Projection's local mode has, by definition, no table and only a keyed batch read. Search answers "find the matching ones", not "give me fields for these keys" — there is no tsvector, no GIN and no facet aggregate to build on a keyed lookup. So the index is a real table in both topologies, and all four Projection guarantees are borrowed by name instead: idempotency on redelivery, ordering by sequence, rebuild from the owner's snapshot, and drift_check.

An event is a signal, not a document. The listing.* payloads are additionalProperties: false and carry identity only, so the indexer pulls the document through a comm Function — in-process in a monolith, over the bus in a split, the same call() either way.

Three engines, one conformance suite

engine typo tolerance facet counts exact total geo synonyms
postgres (default) pg_trgm, second arm exact to the cap, then sampled exact to the cap, then a floor geohash prefilter + haversine query expansion
meili ([meili] extra) native exact exact, a floor past the window native _geoRadius native
naive (tests, SQLite demos) none, declared exact exact python haversine query expansion
opensearch a pointer, not a promise

Differences are never hidden. Each backend declares BackendCapabilities, and every response carries degraded: [...] naming what this engine could not do for this query.

The count is one of those differences, and it says which one it is. count is nullable, count_is_lower_bound marks a floor ("at least N", rendered N+), and exact_total describes THIS answer rather than the engine class — a Postgres candidate set below the cap is counted exactly, and saying otherwise teaches a frontend to distrust a number that is right. The invariant the service enforces for every backend: the answer may never claim fewer matches than the page shows, so count: 0 beside a non-empty items[] is unreachable. Unknown is spelled null, never 0. stapel_search.testing exposes the suite publicly: a new backend without a green conformance run does not merge, and a scenario may only be skipped when the matching capability is False.

The gate: declared ⇒ wired ⇒ covered

The legacy this replaces died in one specific way — features_search, description_en and geohash were written, half-indexed, and read by no query, for years. Three mechanical layers stop that here:

  1. index_schema.py::INDEX_FIELDS is the contract as data, emitted to docs/index.json. The dataclass refuses a field with no read path and no test, so nothing can be declared "for later".
  2. tests/test_index_contract.py runs a round trip per field, against every configured engine, with a mandatory negative half — without one, "finds everything" passes.
  3. stapel-index-lint (in stapel-tools, composed into stapel-verify) enforces the same rules statically across the fleet.

The boundary is stated as plainly as SUR004 states its own: these gates prove the promise was not dropped on the floor, not that the branch is right. Only the round-trip assertions do that.

Ranking is disclosed from the code that ranks

GET /search/api/v1/ranking and docs/ranking.json are rendered from the scorer registry, under the same drift gate as every other artifact — so the P2B Art. 5 disclosure cannot drift from the behaviour the way a paragraph copied into terms of service always does. promoted is serialized on every result item under every sort, including when false (DSA Art. 26), and an explicit sort receives no promotional boost: not a setting, but a structural property of the registry, since promotion_boost declares relevance and nothing else.

Numbers that came from a benchmark, not a feeling

FACET_CANDIDATE_CAP = 15000. Counting remaining facet options is a scan of the candidate set, and the measured curve (tasks/search-facet-benchmark.md) says a single connection stays comfortable to ~130k candidates while eight concurrent clients breach the 200ms target at ~31k. The cap is set with room for concurrency, the TABLESAMPLE fallback is live from day one rather than "when needed", and MAX_FACET_FIELDS = 12 keeps a wide category page from becoming a dozen sequential scans.

License

MIT — see LICENSE.


This page is assembled by stapel-readme from docs/readme.md plus the contract artifacts in docs/. Edit the prose in docs/readme.md; the badges, facts and links above and below it are generated — do not hand-edit README.md.

Metadata

Release files for stapel-search 0.14.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stapel-search 0.14.7
File Size Uploaded
stapel_search-0.14.7.tar.gz 359.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for stapel-search 0.14.7
File Interpreter ABI Platform
stapel_search-0.14.7-py3-none-any.whl Python 3 none any Details

Total release size: 659.2 kB

Release files / stapel_search-0.14.7.tar.gz

Download URL stapel_search-0.14.7.tar.gz
Size 359.8 kB
Tags Source
SHA-256 checksum
How to use checksums
6ede0b47c839945fa47d9a39ba69d6f6b833804b812a0baae25e1c0016e0693f
BLAKE2b-256 checksum
How to use checksums
ac2beb709149d56e484c65ff0c6db87fefa3744ad92e5d24d8f53968fdc5a6bc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.

Transparency log

Release files / stapel_search-0.14.7-py3-none-any.whl

Download URL stapel_search-0.14.7-py3-none-any.whl
Size 299.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aed0ee90d2411be1a09cc9fc243effa9bfd1eac99daf3a0e2dd6b3c44c2a0e09
BLAKE2b-256 checksum
How to use checksums
8f219aaba56b0c3777a45116bb8ebf66b38f2e26f61177db6ad6f57d7697bba8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.

Transparency log

Release history Release notifications | RSS feed

0.18.1

2 release files

0.18.0

2 release files

0.17.2

2 release files

0.17.1

2 release files

0.17.0

2 release files

0.16.8

2 release files

0.16.7

2 release files

0.16.6

2 release files

0.16.5

2 release files

This release

0.14.7 This release

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page