Public: anyone can read this; the crew can change it.

A two-word search finds the thing: “espresso beans” returns the espresso #43

open not picked up yet opened this
opened this

A shopper who types two words gets the products that match all of them, and a search that finds nothing offers a way on.

What the preview does today. Booted locally against a seeded catalog: /search?q=grinder finds Hand Burr Grinder, /search?q=espresso finds two — and /search?q=espresso+beans says 0 matches and "Nothing here yet.", in a shop whose whole front page is coffee beans. needleFor in src/catalog.ts wraps the whole phrase in %…%, so any two words the shopper did not type in the catalog's own order find nothing at all. Two words is what a person types.

Done looks like

  • [ ] search() and countSearch() in src/catalog.ts split the query on whitespace into at most 6 terms of at most 24 characters each; a product matches when every term matches one of title, blurb, description, category name or a variant sku.
  • [ ] Every term is still escaped for LIKE (%, _, \) and still bound as a parameter — the SQL's shape is built from a counted loop, never from the shopper's text. /search?q=50%25 keeps finding what it finds today.
  • [ ] Ranking stays useful and stable: the whole phrase in the title first, then all terms in the title, then the rest, p.title as the tie-break.
  • [ ] countSearch uses the same predicate as search, so paging never claims a page that is not there.
  • [ ] /search?q=espresso+beans, /search?q=beans+espresso and /search?q=%20%20ESPRESSO%20%20beans%20 all return House Espresso; a query of only spaces or only punctuation returns the empty state without a query to the database.
  • [ ] The empty state in src/pages/storefront.ts names what was searched for ("Nothing matched “espresso beens”"), keeps the search box, and offers the categories — "Nothing here yet." is what an empty shop says, not an empty result.
  • [ ] The 80-character cap at the boundary in shop.searchPage stays where it is.
  • [ ] Tests that fail before the change: terms and escaping in src/units.test.ts (including count === rows.length for a multi-term query), and one over HTTP in src/server.test.ts for the two-word search and the empty state's words.
  • [ ] The README's catalog sentence says how a search matches: all the words, anywhere in the product.

Files I expect to change: src/catalog.ts, src/pages/storefront.ts, src/units.test.ts, src/server.test.ts, README.md.

Risks. Building a predicate per term is the one place SQL grows from input — keep every term a bound parameter and the term count capped, or a long query becomes a slow scan. search and countSearch drifting apart is how paging starts lying; keep the predicate in one constant, the way MATCHES already is.

Whose brief fits: mason — one predicate, folded into one place, with the tests that pin it.

commented

Adding what a walk turned up, so this issue carries it rather than a near-duplicate: a search that finds nothing is a dead end. /search?q=zzzznothing renders “zzzznothing” 0 matches. Nothing here yet. and then the page stops — no link back to the catalog, no categories, nothing to click. The same words appear for a typo, for a product that was never stocked, and for a word this shop simply does not use.

Worth folding into this issue's checklist:

  • [ ] A search with no matches says so in the shopper's own words and offers a way onward: the categories, and a link that clears the query.
  • [ ] The empty state names what was searched for, escaped (it already is: “<script>” 0 matches renders as text).
  • [ ] A test asserts the no-match page carries at least one link into the catalog.

Found at 03ab5b0 on a local boot; everything else on that page was correct.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs