AI search

Find icons by describing your use case in natural language.

How It Works

The AI search uses sentence transformers (all-MiniLM-L6-v2) to understand your query and match it against pre-computed embeddings for all 2,146 icons — one vector per icon, not one per drawing.

  • Client-side only: runs in your browser via Transformers.js
  • No API key required: completely free
  • Offline-capable: model cached after first load (~22MB)
  • Fast: ~50ms per search after model loads

Try It Now

Search for icons by describing where you'll use them:

Examples:

  • "shopping cart in header"
  • "user profile sidebar"
  • "navigation menu mobile"
  • "settings gear icon"
  • "delete trash button"
  • "download arrow"

Query Tips

Good Queries Less Effective
"cart in checkout page" "cart"
"user avatar dropdown" "user"
"menu hamburger mobile" "menu"
"notification bell badge" "bell"
"play video button" "play"

Be specific about context: the AI understands semantic relationships.

From the terminal

Terminal
npx @devigner-ui/icons search "upload progress"
npx @devigner-ui/icons search "user profile sidebar" --limit=5 --threshold=0.2
npx @devigner-ui/icons search "map pin" --category=location-maps

The CLI ranks against the same embeddings.json the browser does. Embedding the query needs the model, so install it alongside:

Terminal
npm install @xenova/transformers

Without it the command ranks by keyword and says so on stderr; --keyword asks for that path directly and skips the model load. Full options are in the CLI reference.

In your own code

The package ships the vectors, not a search function; there is no @devigner-ui/icons/ai subpath to import. Ranking is a dot product, so it is shorter to write than to depend on:

TypeScript
import { pipeline } from '@xenova/transformers';
import metadata from '@devigner-ui/icons/metadata.json';
import embeddings from '@devigner-ui/icons/embeddings.json';

async function searchIcons(
  query: string,
  { limit = 10, threshold = 0.15, category = null as string | null } = {},
) {
  const extract = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
    quantized: true,
  });
  const out = await extract(query, { pooling: 'mean', normalize: true });
  const q = Array.from(out.data as Float32Array);

  const pool = category
    ? metadata.icons.filter(i => i.category === category)
    : metadata.icons;

  return pool
    .map(icon => {
      const vec = embeddings[icon.name] ?? [];
      let dot = 0;
      for (let i = 0; i < q.length; i++) dot += q[i] * (vec[i] ?? 0);
      // Stored vectors are unit length scaled by 127; the query is unit
      // length. So this is cosine, in [-1, 1].
      return { icon, score: dot / 127 };
    })
    .filter(r => r.score >= threshold)
    .sort((a, b) => b.score - a.score)
    .slice(0, limit);
}

A lower threshold returns more and worse; 0.15 is where the CLI sits.

Whichever model you use has to be the one that produced the vectors. bge-small is also 384 dimensions, so swapping it in throws nothing; the query just lands in the wrong space and every score becomes noise.

Model Details

Property Value
Model Xenova/all-MiniLM-L6-v2
Dimensions 384
Size (quantized) ~22MB
License Apache-2.0
Runtime Transformers.js (WASM)

Performance

Metric Value
First load ~3-5s (model download)
Subsequent searches ~50ms
Memory usage ~50MB
Accuracy High (semantic understanding)

Privacy

  • No data leaves your browser
  • No tracking: searches are local only
  • Model cached in browser IndexedDB
  • Works offline after first load

Where the vectors come from

scripts/generate-embeddings.mjs builds one string per icon out of its display name, category, keywords and intended use, embeds that, and quantizes the result to int8, which is why embeddings.json is 2MB rather than 3MB of floats.

Quantization is the reason scores divide by 127 rather than comparing directly. The loss is well under the gap between a good match and a bad one.

Documentation

Browse all 8,584 icons