stapel-search
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.1.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 | estimated | geohash prefilter + haversine | query expansion |
meili ([meili] extra) |
native | exact | exact | 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. 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:
index_schema.py::INDEX_FIELDSis the contract as data, emitted todocs/index.json. The dataclass refuses a field with no read path and no test, so nothing can be declared "for later".tests/test_index_contract.pyruns a round trip per field, against every configured engine, with a mandatory negative half — without one, "finds everything" passes.stapel-index-lint(in stapel-tools, composed intostapel-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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stapel_search-0.1.0.tar.gz.
File metadata
- Download URL: stapel_search-0.1.0.tar.gz
- Upload date:
- Size: 155.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
efd8814bf81856a72841d1964644c4edd472abee92f0b5600c4607be644a9282
|
|
| MD5 |
42e12ff6daa4489c163a6ce97c3b8822
|
|
| BLAKE2b-256 |
9780c293e8e9ab9dc6bd948c3dd446bc4251051463a9ac2655b37db396f4e211
|
Provenance
The following attestation bundles were made for stapel_search-0.1.0.tar.gz:
Publisher:
publish.yml on usestapel/stapel-search
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_search-0.1.0.tar.gz -
Subject digest:
efd8814bf81856a72841d1964644c4edd472abee92f0b5600c4607be644a9282 - Sigstore transparency entry: 2568382636
- Sigstore integration time:
-
Permalink:
usestapel/stapel-search@352cce979cee11936a8c822212fe87e377dd08cf -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@352cce979cee11936a8c822212fe87e377dd08cf -
Trigger Event:
push
-
Statement type:
File details
Details for the file stapel_search-0.1.0-py3-none-any.whl.
File metadata
- Download URL: stapel_search-0.1.0-py3-none-any.whl
- Upload date:
- Size: 159.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b02e3cd69e7d5c345978acc3b43281e7f0613dbd84fdb292f803801c6a05bc51
|
|
| MD5 |
af30a74697829d831e292b1c511c0cc3
|
|
| BLAKE2b-256 |
c7dcc5c1db73d28a32da0798511ab1a847589c69f5e9ef9d726f956e04ef7b61
|
Provenance
The following attestation bundles were made for stapel_search-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on usestapel/stapel-search
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_search-0.1.0-py3-none-any.whl -
Subject digest:
b02e3cd69e7d5c345978acc3b43281e7f0613dbd84fdb292f803801c6a05bc51 - Sigstore transparency entry: 2568382661
- Sigstore integration time:
-
Permalink:
usestapel/stapel-search@352cce979cee11936a8c822212fe87e377dd08cf -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@352cce979cee11936a8c822212fe87e377dd08cf -
Trigger Event:
push
-
Statement type: