Next.js guide

Product Search in Next.js with altor-vec

Use altor-vec to add product search to your Next.js 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.

Install: npm install altor-vec @xenova/transformers

Implementation

Uses App Router with 'use client' directive. Uses useRef for the engine, useState for results.

// Next.js App Router — semantic product search (pre-built index)
// app/products/search/page.tsx
'use client';
import { useState, useEffect, useRef } from 'react';
import init, { WasmSearchEngine } from 'altor-vec';
import { pipeline } from '@xenova/transformers';

type Product = { id:number; name:string; price:number; imageUrl:string };

export default function ProductSearchPage() {
  const engine = useRef(null);
  const embedder = useRef(null);
  const [products, setProducts] = useState([]);
  const [results, setResults] = useState([]);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    (async () => {
      const [pRes] = await Promise.all([fetch('/api/products')]);
      const prods: Product[] = await pRes.json();
      setProducts(prods);

      await init();
      embedder.current = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
      const resp = await fetch('/product-search-index.json');
      engine.current = WasmSearchEngine.from_json(await resp.text());
      setReady(true);
    })();
  }, []);

  async function search(q: string) {
    if (!engine.current || q.length < 2) return;
    const out = await embedder.current(q, { pooling: 'mean', normalize: true });
    const hits = JSON.parse(engine.current.search(new Float32Array(out.data), 12));
    setResults(hits.map((h: any) => products[h.id]));
  }

  return (
    
search(e.target.value)} placeholder={ready ? 'Search products by meaning...' : 'Loading...'} />
{results.map(p => (
{p.name}

{p.name}

${p.price}

))}
); }

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 sizeDimensionsQuery p50Memory
1,000 vectors384~0.1ms~2MB
10,000 vectors384~0.4ms~17MB
50,000 vectors384~0.9ms~85MB

When this approach works best

Limitations

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

use case

reference