Building Colour Search for Materia
2025-11-15
Materia is a materials marketplace for architects and designers, and one of the most requested features was searching by colour. Not "filter by red" — proper perceptual colour matching. An architect holds up a fabric swatch and wants to find every material in the catalogue that matches. The problem is that "matching" means different things to different people, and computers are terrible at agreeing with humans about colour.
OKLab or nothing
The first lesson was that HSL is useless for perceptual matching. Two colours can be 5 degrees apart in hue and look identical, or 5 degrees apart and look like completely different colours, depending on where you are in the space. OKLab solves this. It's a perceptual colour space where Euclidean distance actually corresponds to how different two colours look to a human eye. A deltaE of 0.02 is imperceptible. 0.05 is "same family." Above 0.1 and you're in different territory.
The conversion pipeline goes sRGB to linear RGB to OKLab, using the proper matrix transforms. I store both HSL (for categorical filtering) and OKLab (for perceptual matching) on every colour, because they serve different purposes. HSL is good for "show me all the blues." OKLab is good for "show me everything that looks like this specific blue."
Quantisation: 720 buckets
Every product image gets its dominant colours extracted and quantised into a grid: 24 hue steps (every 15 degrees), 6 saturation values, 5 lightness values. That's 720 possible colours. The saturation scale is non-uniform — it includes a 15% step specifically for muted textile colours, which is where a huge chunk of the material catalogue lives. Standard 0/25/50/75/100 misses the entire "slightly desaturated" middle where most fabrics and stones exist.
A critical gotcha: when saturation quantises to zero, hue must be forced to zero too. Otherwise you get grey colours with meaningless hue variations — a "red grey" and a "blue grey" that look identical but map to different buckets.
The JPEG artifact problem
This one was painful. JPEG compression creates saturated pastel artifacts, particularly at certain hues (45, 60, 120, 150, 180, 240, 300 degrees) and 90% lightness. When you're extracting dominant colours from product photography, these artifacts show up as real colours. A white background doesn't extract as white — it extracts as a pale saturated yellow or cyan.
The fix is aggressive: if multiple artifact-coloured buckets sum to more than 30% of the image, or any single artifact colour exceeds 10%, they all get filtered. This sounds heavy-handed but in practice it's the only way to prevent false colour matches from compression noise. Background detection helps too — sampling 5x5 pixels from each corner to identify and exclude the background before quantisation.
Five search modes
The feature grew from "search by colour" into five distinct modes, each with different algorithms:
Single colour search uses a GiST cube index on OKLab coordinates for KNN search. The tolerance slider maps from 0 to 1 using a power curve (x^0.7) that makes the slider feel linear even though the perceptual space isn't. At tolerance 0, threshold is 0.02 (imperceptible difference). At 1.0, it's 0.18 (broad family).
Palette search takes 1-5 colours with AND/OR logic. The algorithm cross-joins palette colours with asset colours, computes minimum distance to each palette colour, and filters by how many of the requested colours match. This required careful SQL — a single CTE with LATERAL joins instead of N EXISTS clauses.
Gradient search was the most interesting to build. Given two endpoint colours, it finds materials that contain colours lying along the gradient line between them. The math projects each colour onto the line using dot products, clamps to [0,1] to prevent extrapolation beyond the endpoints, and measures perpendicular distance. Results come back ordered by position along the gradient, divided into 10 segments to prevent any one region from dominating.
Mood search maps emotional descriptors (warm, moody, luxe, coastal) to regions in OKLab space. Each mood generates three sample colours — a centre point and two variations spread across temperature and lightness — then runs palette search with "any" logic. It's a surprisingly effective bridge between how designers think ("I want something warm") and what the database can search.
Image search lets you upload a photo and search by its extracted palette. The extraction pipeline resizes to 200x200, detects and removes the background, builds a pixel histogram in quantised HSL, computes median colours per bucket, filters JPEG artifacts, merges similar colours within an OKLab threshold of 0.03, and returns up to 12 colours sorted by proportion. Every step exists because something went wrong without it.
Match types matter
Designers gave us a crucial insight: "is this colour" and "includes this colour" are different questions. A marble slab that's 60% white and 40% grey veining is white. But it includes grey. The match type toggle switches between minProportion of 0.5 (dominant) and 0.4 (present), which sounds like a small difference but dramatically changes result quality. This came directly from user research — a designer named Renee articulated the distinction in a way that immediately became the specification.
Caching with quantised keys
Colour search is expensive, so everything goes through Upstash Redis with a circuit breaker pattern. The trick is quantising cache keys to 2 decimal places in OKLab — 0.75:0.02:-0.15 — so visually identical colours hit the same cache entry. A 1-hour TTL is generous because the colour data only changes when we refresh the materialized view of dominant colours.
The circuit breaker is important for a serverless environment. If Redis goes down, the breaker opens after 5 failures, the system falls back to uncached queries for 30 seconds, then retests. Colour search degrades gracefully rather than failing hard.
The lesson
Colour is deceptively complex. What seems like a simple feature — "search by colour" — requires perceptual colour science, image processing, custom quantisation, five different search algorithms, and careful UX to bridge the gap between how designers think about colour and how databases store it. Every shortcut I tried (using HSL distance, skipping artifact filtering, using uniform quantisation) produced results that looked wrong to domain experts, even when they were technically correct.