The till filtered the catalog with one case-insensitive includes() over
sku + description, so word order decided whether you got anything:
"nuts m20" returned 0 hits while "m20 nuts" returned 1. The filter also
checked sku_normalised, brand and search_text — fields that exist on
price_list but not on a stock_items row — so three of its five clauses
could never fire, and name was never searched at all.
Search moves into public/pos/pos-search.js, unit-tested from
src/lib/pos-search.test.ts. Every tier is a strict superset of the old
filter, so nothing that used to match stops matching.
exact whole query as a run
squish punctuation/spacing removed, so M20NUT finds M20 Hex Nut
tokens every word present in any order
plural "nuts" also tries "nut"
glued "M20NUT" -> M20 AND NUT on letter/digit boundaries
Measured on the live 3,591-row catalog: "bolts" 0 -> 884 (884 real bolt
rows, and no row literally says "bolts"), "nuts m20" 0 -> 12, "nut hex"
0 -> 13, "washers flat" 0 -> 14, M20NUT 1 -> 12, zero lost matches.
A plural widens to exactly the union with its singular, asserted per pair.
Widening is safe because both step-2 features are shaped as supersets:
plural forms are alternatives inside one clause, while split parts are
separate required clauses. Making split parts alternatives instead
reduces "M20NUT" to "has M20" — 242 rows. Edit-distance fuzzy matching
was measured and rejected: "nuts" 15 -> 172, "m-20-n-u-t" -> 2,481.
Also fixes three defects the live data caught, all reachable from a code
review alone:
- split kept the generic fragment, promoting "20" from "m20nut" to a
required clause; a leading 1-2 letter run now merges into its digits
- the split group dropped the other tokens, so "M20NUT WASHER" matched
rows containing no washer; other tokens are now carried along
- a punctuation-only query ("-", "---") tokenised to nothing and threw,
which would have blanked the catalog at the till
The index is built once per catalog load on both paths (IndexedDB and
network), keeping derived strings off the items so the cache stays lean.
0.21-2.98 ms per keystroke, so no debounce is needed. filteredProducts()
falls back to the old substring filter if PosSearch is missing, since a
skewed service-worker cache would otherwise blank a shop's catalog.
sw.js v28 -> v30: the new script must be in the app shell, and the second
bump is required because pos-search.js is served cache-first and step 2
edited it in place.