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.2.0
Python >=3.11 (3.11, 3.12, 3.13)
Django djangorestframework>=3.14
HTTP operations 5
Config axes 8
Usage surface 39
Extension points 6
Error codes 54
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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stapel_search-0.2.0.tar.gz (161.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stapel_search-0.2.0-py3-none-any.whl (163.3 kB view details)

Uploaded Python 3

File details

Details for the file stapel_search-0.2.0.tar.gz.

File metadata

  • Download URL: stapel_search-0.2.0.tar.gz
  • Upload date:
  • Size: 161.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stapel_search-0.2.0.tar.gz
Algorithm Hash digest
SHA256 9f6d862d63d055f7f539523f295c97d71702668df24f019d86849932bfedc79c
MD5 b0f7f7f6e35666278ed6bc501e83810f
BLAKE2b-256 fa9c30bfceb1e5390b019c4c37e7b757a5449f7458f3da4b600a95995ee0eb08

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_search-0.2.0.tar.gz:

Publisher: publish.yml on usestapel/stapel-search

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stapel_search-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: stapel_search-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 163.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stapel_search-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 047211304063f88c9dba4359ed0e337c6aad949b9fc220ce8eb4915d978a245d
MD5 d3be6860cb8da6e9f9c96f0b544f4cd0
BLAKE2b-256 4ddfb7b349d3d416cd08d50b4ffdf56d69cf5dad516ccea8a1a6f041efcc731c

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_search-0.2.0-py3-none-any.whl:

Publisher: publish.yml on usestapel/stapel-search

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.2

2 files

0.2.1

2 files

This release

0.2.0 This release

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page