Maville Laptop Search
Intent-aware laptop search: a 16GB query returns 23 more usable options across 112 laptops
$ deploy --project maville-laptop-search
HTTP/2 200 OK
Key Technical Highlights
On real inventory a 16GB search went from 77 results to 77 exact + 23 upgradeable laptops, ranked after exact matches and labelled "8GB -> 16GB" or "ask us" when the maximum RAM is not recorded. I chose to surface upgrade paths rather than pad results, so the label never promises what the data cannot back.
Search parses free text ("hp elitebook 16gb touchscreen") into structured RAM, storage and feature filters plus per-term matching with synonyms, shared by the query builder and the UI. I rejected a hosted search engine: a catalogue of about 112 rows does not justify another service, and the parser keeps the logic testable.
Backfilled series for 112 of 112 laptops and replaced free-text admin entry with a DB-backed controlled vocabulary (brand to series dependent, select-or-add). Root cause of broken filters was data entry ("HP " with a trailing space, one CPU spelled 4+ ways), so I fixed the input, not just the query.
Contact and newsletter forms now store the enquiry in Postgres first, then send via Resend, so a failed email never loses a lead; unsubscribe links are HMAC-signed with RFC 8058 one-click headers. Schema change checked as purely additive against production before shipping.
Every laptop page emits Product + Offer + BreadcrumbList JSON-LD (price, currency, condition, availability) with a DB-generated sitemap, robots rules and noindex on personalised pages, so listings are eligible for rich results; indexing outcomes are not yet measured.
The Case Study
The Problem
Maville sells new and UK-used laptops in Nigeria through a browse-and-enquire model: shoppers filter the catalogue, then close the sale on WhatsApp. A written site audit (29 July 2026) of the live catalogue (112 laptops) flagged the exact weaknesses I went on to fix: no keyword search, and a brand filter that listed HP and Lenovo twice. Shoppers also search the way they talk ("16gb touchscreen thinkpad"), and plain substring matching cannot read that.
The Numbers
- 16GB search on real inventory: 77 results before, 77 exact + 23 upgradeable after.
- Series backfilled for 112 of 112 existing laptops.
- 11 commits from first commit (26 July 2026) to the latest (16 September 2026), all authored by me.
- Lead pipeline: store first, email second, one-click unsubscribe on every newsletter send.
Decisions and Trade-offs
Parse intent, don't substring-match. A parser turns free text into RAM, storage and feature filters plus per-term matches with synonyms ("ryzen" implies AMD). I rejected a hosted search service: at about 112 rows it adds cost and a failure mode for no gain. Trade-off: synonym lists are hand-maintained.
Show upgradeable laptops, but label them honestly. A shopper asking for 16GB is often happy with an 8GB machine that takes a 16GB upgrade. I included those, ranked them after exact matches, and show "ask us" instead of a figure when maximum RAM is not recorded. I rejected silently mixing them in, which would have over-promised.
Fix the data at the input. The filters were broken by free-text admin entry. Instead of loosening the queries, I moved to a controlled vocabulary: options merge a built-in catalog, admin-added values and values already in inventory. Only model name and price are typed; series and CPU generation are derived. Trade-off: one tolerant brand match remains for legacy rows.
Store the lead before emailing it. Forms write to Postgres first, then call the email API. I rejected email-only delivery because an outage would silently drop a sales enquiry. Phone numbers are normalised for WhatsApp broadcasts.
Structured data per listing. Product, Offer and Breadcrumb JSON-LD are generated from the same database rows as the page, so price and availability cannot drift from what is shown.
How It Was Verified
The 77 and 23 figures come from running the new query against the real inventory, not fixtures. I confirmed the schema change was additive with a migrate diff against production before shipping. There is no automated test suite in the repo: verification was manual runs against live data.
Honest Limits and What I'd Change
- No unit tests; the parser and upgrade rules are pure functions and are the first thing I would cover.
- Search is substring-based Postgres filtering; fine at this size, but it would need full-text or trigram indexing as the catalogue grows.
- I have no Search Console or PageSpeed data, so I make no claims about rankings or Core Web Vitals.
- The synonym list and the "max RAM unknown" ceiling are heuristics that need review as stock changes.
My Role
Sole engineer: search and filtering, the data model and vocabulary, admin tooling, lead pipeline, SEO plumbing and deployment on Vercel, built for Maville Technologies.