Rebasely Beta: data may be purged

examples/storefront Public

Markdown · 51 lines · 2.7 KiB · at b748afa

---id: searchtype: conceptowner: justincomponent: searchdescribes:  - src/search.ts  - search/migrations/**reviewed: 2026-10-16---# Search`GET /search?q=linen apr` returns active products whose name or description match, best match first:```GET /search?q=linen%20apr&limit=24{ "query": "linen apr", "products": [{ "id": "linen-apron", "name": "Linen work apron", ... }] }````q` is 1 to 100 characters. `limit` works as on `GET /products`: 1 to 100, default 24. There is no paging; a shopper who does not find it in the first page types more.## The indexThe index is one FTS5 table, `products_fts`, in its own D1 database, `storefront-search` ([[0011-search-index-in-its-own-database]]). It has a row per active product with the product's id (stored, not searched), name and description. Nothing else about a product is in it.The tokenizer is `porter unicode61 remove_diacritics 2`: `mugs` finds `mug`, and `creme` finds `Crème`.## Queries`toMatch()` turns what the shopper typed into an FTS5 query. It keeps letters and digits, quotes each word, and makes the last one a prefix:| typed | MATCH || --- | --- || `linen apr` | `"linen" "apr"*` || `apron" OR name:mug` | `"apron" "OR" "name" "mug"*` || `***` | nothing; the answer is an empty list |Quoting means FTS5 operators and column filters in the input are only words, so no input can make the query fail. The prefix on the last word lets results follow someone who is still typing. Up to eight words are used.Matches are ranked with `bm25(products_fts, 0, 10, 1)`: a word in the name counts ten times as much as one in the description, so the apron comes before the tote whose description mentions an apron.## Reading the resultsThe index only answers with ids. `activeProducts()` (`src/catalog.ts`) reads those products from the catalog with `json_each`, keeps the active ones and returns them in the index's order. Price and stock are therefore always the catalog's, including stock that checkout took a second ago, and a product retired since it was indexed is left out.## Keeping it in step- `PUT /admin/products/:id` saves the product, then calls `indexProduct()`, which deletes the product's row and inserts it again in one batch, or only deletes it when the product is inactive. If that fails, the answer is `503 search_index_failed`; the product is saved, and sending the same request again fixes the index.- `POST /admin/search/rebuild` (admin token) empties the index and fills it from every active product, 50 at a time, and answers `{ "indexed": n }`. Use it for a new search database, after `npm run db:seed`, or whenever results look wrong. While it runs, searches can miss products it has not reached yet.Image uploads and checkout do not touch the index: they change nothing search matches on.