add LIKE substring fallback to search and update plan statuses
FTS5 prefix matching misses mid-word substrings (e.g. "ebook" in "notebook"). When FTS5 returns zero results, fall back to LIKE query on title and category with proper wildcard escaping. 4 new tests, 757 total. Also marks completed plan files (search, admin-redesign, setup-wizard, products-context) with correct status. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
57c3ba0e28
commit
edcbc596e3
15
PROGRESS.md
15
PROGRESS.md
@ -21,7 +21,7 @@
|
||||
- Transactional emails (order confirmation, shipping notification)
|
||||
- Demo content polished and ready for production
|
||||
|
||||
**Tier 1 MVP complete.** CI pipeline done. Hosting & deployment done (including observability). PageSpeed CI done (99-100 mobile, 97+ desktop). Usability fixes 15/18 done (remaining 3 are now tracked as features below).
|
||||
**Tier 1 MVP complete.** CI pipeline done. Hosting & deployment done (including observability). PageSpeed CI done (99-100 mobile, 97+ desktop). Usability fixes 16/18 done (remaining 2 are now tracked as features below).
|
||||
|
||||
## Task list
|
||||
|
||||
@ -60,9 +60,9 @@ Plans: [admin-redesign.md](docs/plans/admin-redesign.md) | [setup-wizard.md](doc
|
||||
|
||||
**Total remaining: ~27-33 hours across ~12 sessions**
|
||||
|
||||
## Usability fixes (15/18 done)
|
||||
## Usability fixes (16/18 done)
|
||||
|
||||
Issues from hands-on testing of the deployed prod site (Feb 2025). 15 of 18 complete. The remaining 3 are tracked as features in the task list above (#5 search, #16 variant refinement, #18 shipping costs).
|
||||
Issues from hands-on testing of the deployed prod site (Feb 2025). 16 of 18 complete. The remaining 2 are tracked as features in the task list above (#16 variant refinement, #18 shipping costs).
|
||||
|
||||
## Roadmap
|
||||
|
||||
@ -296,9 +296,13 @@ All shop pages now have LiveView integration tests (612 total):
|
||||
- [x] Index auto-rebuilds after each provider sync
|
||||
- [x] 18 search tests, 744 total
|
||||
|
||||
**Follow-ups:**
|
||||
**Follow-ups (all complete):**
|
||||
- [x] Wire shop LiveViews to direct DB queries (PreviewData removed from all shop pages, cart, error page)
|
||||
- [ ] Keyboard navigation in search modal (up/down arrows, Enter to navigate)
|
||||
- [x] Search modal keyboard nav (arrow keys, Enter, Escape, Cmd+K shortcut)
|
||||
- [x] Full ARIA combobox pattern (role=combobox, listbox, option, aria-selected)
|
||||
- [x] SearchModal JS hook, `<.link navigate>` for client-side nav, 150ms debounce
|
||||
- [x] search.ex: transaction safety on reindex, public `remove_product/1`
|
||||
- [x] 10 new integration tests, 755 total
|
||||
|
||||
### Page Editor
|
||||
**Status:** Future (Tier 4)
|
||||
@ -313,6 +317,7 @@ See: [docs/plans/page-builder.md](docs/plans/page-builder.md) for design
|
||||
|
||||
| Feature | Commit | Notes |
|
||||
|---------|--------|-------|
|
||||
| DB wiring + search UX | 57c3ba0 | Shop LiveViews use DB queries, search keyboard nav, ARIA, 755 tests |
|
||||
| FTS5 search + products refactor | 037cd16 | FTS5 index, BM25 ranking, search modal, denormalized fields, Product struct usage, 744 tests |
|
||||
| PageSpeed CI | 516d0d0 | `mix lighthouse` task, prod asset build, gzip, 99-100 mobile scores |
|
||||
| Observability | eaa4bbb | LiveDashboard in prod, ErrorTracker, JSON logging, Oban/LV metrics, os_mon |
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Plan: Admin area redesign
|
||||
|
||||
Status: Pending
|
||||
Status: Complete
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Plan: Products Context with Provider Integration
|
||||
|
||||
> **Status:** Phase 1 Complete (c5c06d9) - See [PROGRESS.md](../../PROGRESS.md) for current status.
|
||||
> **Status:** Complete (c5c06d9, 037cd16, 57c3ba0) - See [PROGRESS.md](../../PROGRESS.md) for current status.
|
||||
|
||||
## Goal
|
||||
Build a Products context that syncs products from external POD providers (Printify first), stores them locally, and enables order submission for fulfillment.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Plan: Implement product search in search modal
|
||||
|
||||
Status: Pending (after usability fixes)
|
||||
Status: Complete (037cd16, 57c3ba0)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Plan: Setup wizard and "go live" gate
|
||||
|
||||
Status: Pending
|
||||
Status: Complete
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@ -27,7 +27,11 @@ defmodule SimpleshopTheme.Search do
|
||||
[]
|
||||
else
|
||||
fts_query = build_fts_query(query)
|
||||
search_fts(fts_query)
|
||||
|
||||
case search_fts(fts_query) do
|
||||
[] -> search_like(query)
|
||||
results -> results
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -135,6 +139,26 @@ defmodule SimpleshopTheme.Search do
|
||||
end
|
||||
end
|
||||
|
||||
# Substring fallback when FTS5 prefix matching returns nothing
|
||||
defp search_like(query) do
|
||||
pattern = "%#{sanitize_like(query)}%"
|
||||
|
||||
Product
|
||||
|> where([p], p.visible == true and p.status == "active")
|
||||
|> where([p], like(p.title, ^pattern) or like(p.category, ^pattern))
|
||||
|> order_by([p], p.title)
|
||||
|> limit(20)
|
||||
|> preload(^@listing_preloads)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
defp sanitize_like(input) do
|
||||
input
|
||||
|> String.replace("\\", "\\\\")
|
||||
|> String.replace("%", "\\%")
|
||||
|> String.replace("_", "\\_")
|
||||
end
|
||||
|
||||
defp insert_into_index(%Product{} = product) do
|
||||
Repo.query!(
|
||||
"INSERT INTO products_search_map (product_id) VALUES (?1)",
|
||||
|
||||
@ -169,18 +169,24 @@ defmodule SimpleshopTheme.SearchTest do
|
||||
|
||||
describe "index_product/1" do
|
||||
test "indexes a single product", %{ocean: ocean} do
|
||||
# Clear and rebuild without ocean
|
||||
# Clear FTS index
|
||||
SimpleshopTheme.Repo.query!("DELETE FROM products_search_map")
|
||||
SimpleshopTheme.Repo.query!("DELETE FROM products_search")
|
||||
|
||||
assert Search.search("ocean") == []
|
||||
# Verify FTS index is empty
|
||||
%{rows: rows} = SimpleshopTheme.Repo.query!("SELECT COUNT(*) FROM products_search_map")
|
||||
assert rows == [[0]]
|
||||
|
||||
# Index just ocean
|
||||
ocean = SimpleshopTheme.Repo.preload(ocean, [:variants])
|
||||
Search.index_product(ocean)
|
||||
|
||||
# Verify ocean is now in the FTS index
|
||||
%{rows: [[count]]} = SimpleshopTheme.Repo.query!("SELECT COUNT(*) FROM products_search_map")
|
||||
assert count == 1
|
||||
|
||||
results = Search.search("ocean")
|
||||
assert length(results) == 1
|
||||
assert length(results) >= 1
|
||||
assert hd(results).id == ocean.id
|
||||
end
|
||||
|
||||
@ -197,13 +203,41 @@ defmodule SimpleshopTheme.SearchTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "LIKE fallback" do
|
||||
test "finds substring matches that FTS5 prefix misses", %{ocean: ocean} do
|
||||
# "ebook" is in the middle of "Notebook" — FTS5 prefix won't match
|
||||
results = Search.search("ebook")
|
||||
assert Enum.any?(results, &(&1.id == ocean.id))
|
||||
end
|
||||
|
||||
test "falls back to category substring match", %{forest: forest} do
|
||||
# "ppar" is a substring of "Apparel" — FTS5 prefix won't match
|
||||
results = Search.search("pparel")
|
||||
assert Enum.any?(results, &(&1.id == forest.id))
|
||||
end
|
||||
end
|
||||
|
||||
describe "remove_product/1" do
|
||||
test "removes a product from the index", %{ocean: ocean} do
|
||||
assert Search.search("ocean") != []
|
||||
# Verify ocean is in the FTS index
|
||||
%{rows: [[rowid]]} =
|
||||
SimpleshopTheme.Repo.query!(
|
||||
"SELECT rowid FROM products_search_map WHERE product_id = ?1",
|
||||
[ocean.id]
|
||||
)
|
||||
|
||||
assert rowid
|
||||
|
||||
Search.remove_product(ocean.id)
|
||||
|
||||
assert Search.search("ocean") == []
|
||||
# Verify it's gone from the FTS index
|
||||
%{rows: rows} =
|
||||
SimpleshopTheme.Repo.query!(
|
||||
"SELECT rowid FROM products_search_map WHERE product_id = ?1",
|
||||
[ocean.id]
|
||||
)
|
||||
|
||||
assert rows == []
|
||||
end
|
||||
|
||||
test "is a no-op for unindexed product" do
|
||||
|
||||
Loading…
Reference in New Issue
Block a user