React guide
Product Search in React with altor-vec
Use altor-vec to add product search to your React app — entirely in the browser, with no server, no API keys, and zero per-query cost. Search a product catalog by semantic meaning — find products by concept, synonym, or intent rather than requiring exact keyword matches.
npm install altor-vec @xenova/transformersImplementation
Works with Vite, CRA, or any React 18+ setup. Uses useState + useRef for the engine.
// Product search with post-filter — React hook
import { useState, useEffect, useRef, useCallback } from 'react';
import init, { WasmSearchEngine } from 'altor-vec';
import { pipeline } from '@xenova/transformers';
type Product = { id:number; name:string; description:string; category:string; price:number };
export function useProductSearch(products: Product[]) {
const engine = useRef(null);
const embedder = useRef(null);
const [ready, setReady] = useState(false);
useEffect(() => {
(async () => {
await init();
embedder.current = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
const DIM = 384;
const vecs = new Float32Array(products.length * DIM);
for (const [i, p] of products.entries()) {
const out = await embedder.current(`${p.name}. ${p.description}`,
{ pooling: 'mean', normalize: true });
vecs.set(out.data, i * DIM);
}
engine.current = WasmSearchEngine.from_vectors(vecs, DIM, 16, 200, 50);
setReady(true);
})();
}, []);
const search = useCallback(async (
query: string,
filters?: { category?: string; maxPrice?: number }
) => {
if (!engine.current) return [];
const out = await embedder.current(query, { pooling: 'mean', normalize: true });
// Over-fetch to allow for post-filtering
const hits = JSON.parse(engine.current.search(new Float32Array(out.data), 50));
let results = hits.map((h: any) => products[h.id]);
if (filters?.category) results = results.filter(p => p.category === filters.category);
if (filters?.maxPrice) results = results.filter(p => p.price <= filters.maxPrice);
return results.slice(0, 8);
}, []);
return { search, ready };
}
Performance
50K products at 384 dimensions: ~85MB memory, ~1ms per query. Measured on M2 MacBook Pro, Chrome 124. Mobile is typically 2–4× slower — test on target devices before deploying.
| Index size | Dimensions | Query p50 | Memory |
|---|---|---|---|
| 1,000 vectors | 384 | ~0.1ms | ~2MB |
| 10,000 vectors | 384 | ~0.4ms | ~17MB |
| 50,000 vectors | 384 | ~0.9ms | ~85MB |
When this approach works best
- Static e-commerce catalogs (Shopify/Stripe products exported as JSON)
- Apps where search must work offline or with zero API budget
- Product discovery UIs where semantic matching improves conversion
Limitations
- No built-in faceted filtering (category, price range) — implement with post-filter on results
- Index updates require a rebuild step — not suitable for catalogs that change hourly
Frequently asked questions
How do I filter by category or price after a semantic search?
Run engine.search(queryEmbedding, 50) to get 50 candidates, then filter the results array by category, price range, or in-stock status in JavaScript before showing the top N to the user. This is called post-retrieval filtering.
Will semantic search understand synonyms like 'sneakers' vs 'trainers'?
Yes. Embedding models encode semantic meaning, so 'sneakers', 'trainers', 'running shoes', and 'athletic footwear' will all map to nearby vector positions and return similar results.
How do I generate embeddings for product titles and descriptions?
Concatenate the product name and description: `${product.name}. ${product.description}`. Embed this combined string with all-MiniLM-L6-v2 via Transformers.js. This gives better results than embedding the title alone.
Related resources
framework
reference