Two Ways to Find an Icon Among 280,000

Two Ways to Find an Icon Among 280,000

Why we needed a search at all

Jinero's Icon Library holds 280,101 free open-source SVG icons across 237 packsLucide, Heroicons, Tabler, Material Symbols, Phosphor, IconPark, Bootstrap Icons and dozens more. You can filter them, preview any icon full-size, copy it as SVG, export it as PNG in eight sizes, gather a selection and ship it as one sprite, icon font or ZIP with the Icon Pack Generator. The same catalogue feeds our public API, the Figma plugin and the MCP server.

That scale is the whole problem.

A pack of 300 icons doesn't need search — you scroll it. At 280,000 the catalogue is only as good as the way in. And "the way in" turns out to be two completely different problems wearing the same input field.

The first problem is speed. You type arrow, you expect results before you finish the word. That is a database question.

The second problem is vocabulary. You need an icon for "the user is logging out" and the icon you want is called sign-out-alt in one pack, logout in another, exit_to_app in a third and door-open in a fourth. You don't know any of those names. That is not a database question at all.

We solved them separately, and the interesting part is why they couldn't be solved together.


Method 1 — keyword search, and why it isn't just LIKE

The default search is instant and runs on every keystroke. It has to be a database query — nothing else answers in tens of milliseconds across 280k rows.

The naive version is WHERE name LIKE '%arrow%'. It's also useless: it can't rank, it can't handle two words, and it scans the whole table.

What actually runs is a MySQL boolean FULLTEXT query against a dedicated column, search_keywords. Every token of three characters or more becomes a required prefix match:

"shopping cart"  →  +shopping* +cart*

Tokens shorter than three characters fall back to a LIKE on the name, because FULLTEXT ignores them. We cap the query at eight tokens — beyond that a user isn't searching, they're pasting.

Measured on production:

Query Time Results
delete 37 ms 2,143
sign out 41 ms 420
shopping cart 37 ms 1,746

The column that does the real work

search_keywords is not the icon's name. It's a synonym-enriched blob built at ingest time, and it's where most of the quality lives.

We keep 313 synonym groups, each one collapsing a concept into every word designers actually use for it:

delete → trash → remove → bin → erase
edit   → modify → pencil → pen → write
add    → plus → create → new → insert

Type delete and you get icons named trash, bin and remove, because at ingest all of those words were written into the same searchable blob. It costs nothing at query time — the work happened once, offline.

There's a special case worth mentioning because it broke in a way we didn't expect. Flag icons are named by ISO code: Jp, Ua, De. Nobody searches for "Jp" — they search for "Japan". So for flag packs we bake the country name and its aliases into the keywords.

But only for flag packs. The first version did it globally, and the result was absurd: the ISO codes in, it, is and no are ordinary English words, so searching for "no entry" started returning Norwegian and Italian flags. The fix wasn't cleverer matching — it was restricting the country map to kind=flag and leaving the rest of the catalogue alone.

Where keyword search stops

Ask it for something you can describe but can't name:

"something went wrong"   →   nothing found

Not a bad result. No result. There is no icon in 280,000 whose keywords contain that phrase, and there never will be, because it describes a situation rather than a picture.

That is the wall. You cannot synonym your way out of it — you'd need a synonym group for every thought a person might have. This is where the second method starts.


Method 2 — search by what the icon looks like

The second search doesn't read names. It looks at the picture.

The idea is the same one behind our Font Recognizer: turn the thing into a vector of numbers, then compare numbers. But where the recognizer compares image to image, here we need to compare text to image — your sentence against a drawing.

That is exactly what a vision-language model does. It embeds pictures and text into one shared space, so "a bin with a lid" and the actual drawing of a bin land close together.

The model we didn't train

We trained our own model for the Font Recognizer, and it beat every off-the-shelf option. So the first instinct here was the same: train a contrastive model on our own icons.

We started, and then we stopped.

Fonts and icons are different problems. Typefaces differ by hair-thin details — the shape of a terminal, the axis of a bowl — and no general-purpose model is trained to care about that. Icons are the opposite: they're pictures of ordinary things. A bin, an arrow, a shopping cart. General-purpose vision-language models have seen millions of those.

So we use SigLIP2 (so400m-patch16-384) unchanged. Training our own would have cost weeks to arrive somewhere behind it. The honest engineering answer was to not build the thing.

That is the mirror image of the font recognizer story, and both decisions came from the same question: is the general model actually bad at this? For fonts it was. For icons it isn't.

What runs in production

Every icon in the index was passed through SigLIP once, offline, producing a vector of 1,152 numbers:

siglip_index.npz     259,301 × 1152, float16     586 MB
service RAM          ~1.2 GB

A separate always-on container holds the model and the whole index in memory. A query embeds your text — that's the only model call — and then it's a dot product against a quarter-million rows.

Endpoint Time
/search (text → icon) 257–343 ms
/similar (icon → icon) 35 ms

The text search is an order of magnitude slower because encoding your sentence is the expensive part; comparing vectors afterwards is nearly free. That's also why "Similar" is so fast: the vector already exists, so there's no model call at all.

Which is why Deep search is a button, not the default. 300 ms is fine when you press it deliberately; it is unusable as-you-type. Keyword search stays instant, Deep is opt-in.

It doesn't care what language you ask in

This is the part that surprises people, and it isn't a feature we built. It comes free with the model, because SigLIP learned text and images together across many languages.

The same question, three languages, actual results from production:

Query Time Top results
throw it away 257 ms Reuse · Garbage Throw · Litter In Bin Sign · Bin
викинути у смітник 302 ms Garbage Throw · Litter In Bin Sign · Bin
ゴミ箱に捨てる 343 ms Delete · Delete Sweep · Trash · Trashcan

Not one of those icons has a Ukrainian or Japanese word anywhere in its metadata. The catalogue is entirely English. The model isn't matching words — it's matching meaning to picture.

The two searches don't replace each other — they merge

Deep search doesn't discard the keyword results. It runs both and unions them: semantic hits first, keyword hits after, filters applied while preserving that order, cached for five minutes so pagination doesn't re-run the model.

And if the search service is down, the toggle silently falls back to keyword. A slower search is a feature; a broken search is an outage.


Find Similar — the same index, the other direction

Every icon's detail view has a Similar button. It takes that icon's vector — already in the index — and returns its nearest neighbours across all 280,000, in 35 ms. Useful for finding a matching style, or a better-drawn version of the icon you found.

It also taught us something worth stating plainly, because it's the clearest limitation in the whole system.

Ask for icons similar to a full trash bin and you get:

Pail · Accumulation Precipitation · Glas Water · Specific Gravity

A pail. A container of water. These are not semantically related to deleting things — but they are absolutely, undeniably similar-looking. A bin and a bucket are the same shape.

That is what image-to-image similarity means, and it's worth being clear about: "Similar" finds icons that look alike, not icons that mean alike. When you're matching a visual style that's exactly right. When you expect "more delete icons", it isn't.


What it still gets wrong

Deep search is not magic, and the failure mode is specific: it's good at things and bad at situations.

"something went wrong"  →  Compass South West · Compass West Arrow · Roadmap · Phone Off

Keyword search returns nothing here; Deep returns four confident wrong answers. Which of those is worse is a genuine UX question. An empty result tells you to rephrase. A wrong result tells you the catalogue doesn't have what you want — and that's a lie.

Concrete objects work: a bin with a lid, bicycle, wifi off. Abstractions drift: error states, emotions, "professional-looking".

There's also a structural property worth understanding, because it explains the occasional gap between the two searches.

A vector index describes the asset as it was when the index was built, not as it is now. Keyword search reads the database, so a newly ingested pack is searchable the moment it lands. Deep search reads a file of pre-computed vectors, so new icons join it at the next rebuild. That's the trade for answering in 300 ms instead of 300 seconds.

We learned how literal that is the hard way. At one point 616 IconPark icons were rendering as solid black shapes — a mask-knockout bug in the source SVGs. The drawings were repaired, and anyone browsing the library sees them correctly. But an embedding computed from the broken render encodes exactly that: a black rectangle. The picture and its description of the picture are two different artefacts, and fixing one does not fix the other. Re-embedding after a pack changes is part of the ingest routine for precisely this reason.


Where each one is wired in

Not everywhere, deliberately.

The public API, the Figma plugin and the MCP server are keyword-only. They're machine-facing: an integration wants a fast, predictable, cheap answer, and it usually already knows the word it's looking for. Semantic search costs ~300 ms and an always-on model sitting in memory; we'll enable it there when someone actually needs it, with its own rate limits.

Deep search and Similar live in the web library, where a human is looking at a grid and thinking "not that — something more like this".


The takeaway

Two searches, because there are two questions.

When you know the word, the database wins — 37 ms, ranked, and it runs on every keystroke. Most searches are this. The work that makes it good happened offline, in a synonym table, not at query time.

When you only know the meaning, the model wins — and it answers in a language the catalogue has never been translated into.

The engineering decision we're most comfortable with is the one where we wrote no model at all. For fonts, training our own beat everything available. For icons, the best available model was already better than what we'd have built. Knowing which situation you're in is most of the job.


20 views 0 comments

Comments 0

Sign in to be the first to comment.