code example
Build Product Search with altor-vec
What this pattern solves: Natural-language retrieval for e-commerce catalogs without a dedicated search backend.
Shoppers rarely type exact SKU language. They describe goals such as 'gift for remote coworker' or 'soft black running shorts with pockets' and expect relevant products even if those words never appear together in the title.
A browser index works well for curated, seasonal, or edge-cached catalogs where fast intent matching matters more than infinite scale. The experience stays responsive even on flaky mobile connections.
Install
npm install altor-vec
Concept explanation
In a product search workflow, users usually describe intent in their own words. That is why vector search works well here: each record is turned into an embedding, the embeddings are indexed once, and later queries retrieve the nearest semantic neighbors instead of relying only on exact tokens. In practice this means the interface can respond to paraphrases, shorthand, and partial descriptions far better than a literal-only search box.
The browser is often the right place to do this when the corpus is moderate in size and safe to ship. The instant benefit is lower latency. The architectural benefit is that you remove a whole search service from the request path. That matters for keystroke-heavy interactions, offline-capable apps, and product surfaces where search should feel like a UI primitive rather than a network round trip.
This page uses a deterministic embedding helper so the sample is runnable with only altor-vec installed. That keeps the example honest and easy to paste into a demo project. Combine embeddings with metadata filters such as price band, availability, or category when you move from proof of concept to production search results.
Runnable JavaScript example
The following snippet indexes a small in-memory dataset, performs a semantic lookup for wireless keyboard for travel, and prints the nearest matches. It uses the real altor-vec API, including init(), WasmSearchEngine.from_vectors(), and search().
import init, { WasmSearchEngine } from 'altor-vec';
const dims = 12;
const records = [
{
"title": "Foldable Bluetooth keyboard",
"text": "Pocket-sized wireless keyboard for tablets, travel desks, and remote work kits.",
"meta": "accessories"
},
{
"title": "Mechanical desk keyboard",
"text": "Full-size tactile keyboard with hot-swap switches and RGB lighting.",
"meta": "peripherals"
},
{
"title": "USB-C laptop stand",
"text": "Portable stand that lifts a notebook for better posture on the road.",
"meta": "ergonomics"
},
{
"title": "Travel mouse",
"text": "Compact quiet wireless mouse with USB-C charging and slim carry case.",
"meta": "accessories"
},
{
"title": "Noise-canceling headset",
"text": "Wireless over-ear headset for calls, focus time, and noisy cafes.",
"meta": "audio"
},
{
"title": "Screen cleaning kit",
"text": "Microfiber and spray kit for monitors, tablets, and phones.",
"meta": "care"
}
];
function embedText(text) {
const vector = new Float32Array(dims);
for (const token of text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)) {
let hash = 2166136261;
for (const char of token) {
hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
}
const slot = Math.abs(hash) % dims;
vector[slot] += 1;
vector[(slot + token.length) % dims] += token.length / 10;
}
const magnitude = Math.hypot(...vector) || 1;
return Array.from(vector, (value) => value / magnitude);
}
async function main() {
await init();
const flat = new Float32Array(
records.flatMap((record) => embedText(`${record.title} ${record.text} ${record.meta}`))
);
const engine = WasmSearchEngine.from_vectors(flat, dims, 16, 200, 64);
const hits = JSON.parse(engine.search(new Float32Array(embedText('wireless keyboard for travel')), 4));
const results = hits.map(([id, distance]) => ({
...records[id],
similarity: Number((1 - distance).toFixed(3)),
}));
console.table(results);
engine.free();
}
main();
React component version
The React version keeps the same index build but wires it into component state so the UI can query on input changes. That is usually how teams introduce semantic retrieval into an existing product: initialize once, keep the engine in memory, and map nearest-neighbor hits back to the original records.
import { useEffect, useState } from 'react';
import init, { WasmSearchEngine } from 'altor-vec';
const dims = 12;
const records = [
{
"title": "Foldable Bluetooth keyboard",
"text": "Pocket-sized wireless keyboard for tablets, travel desks, and remote work kits.",
"meta": "accessories"
},
{
"title": "Mechanical desk keyboard",
"text": "Full-size tactile keyboard with hot-swap switches and RGB lighting.",
"meta": "peripherals"
},
{
"title": "USB-C laptop stand",
"text": "Portable stand that lifts a notebook for better posture on the road.",
"meta": "ergonomics"
},
{
"title": "Travel mouse",
"text": "Compact quiet wireless mouse with USB-C charging and slim carry case.",
"meta": "accessories"
},
{
"title": "Noise-canceling headset",
"text": "Wireless over-ear headset for calls, focus time, and noisy cafes.",
"meta": "audio"
},
{
"title": "Screen cleaning kit",
"text": "Microfiber and spray kit for monitors, tablets, and phones.",
"meta": "care"
}
];
function embedText(text) {
const vector = new Float32Array(dims);
for (const token of text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)) {
let hash = 2166136261;
for (const char of token) {
hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
}
const slot = Math.abs(hash) % dims;
vector[slot] += 1;
vector[(slot + token.length) % dims] += token.length / 10;
}
const magnitude = Math.hypot(...vector) || 1;
return Array.from(vector, (value) => value / magnitude);
}
export function ProductSearchExample() {
const [engine, setEngine] = useState(null);
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false;
let instance;
(async () => {
await init();
const flat = new Float32Array(
records.flatMap((record) => embedText(`${record.title} ${record.text} ${record.meta}`))
);
instance = WasmSearchEngine.from_vectors(flat, dims, 16, 200, 64);
if (!cancelled) setEngine(instance);
})();
return () => {
cancelled = true;
instance?.free();
};
}, []);
useEffect(() => {
if (!engine || query.trim().length < 2) {
setResults([]);
return;
}
const hits = JSON.parse(engine.search(new Float32Array(embedText(query)), 5));
setResults(
hits.map(([id, distance]) => ({
...records[id],
similarity: Number((1 - distance).toFixed(3)),
}))
);
}, [engine, query]);
return (
<section>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search products semantically"
/>
<ul>
{results.map((result) => (
<li key={result.title}>
<strong>{result.title}</strong> — {result.meta} (score {result.similarity})
</li>
))}
</ul>
</section>
);
}
How this example works
The pattern has three moving parts. First, you choose what text represents each record: title, description, metadata, or a chunk of content. Second, you turn that text into vectors and flatten them into one Float32Array. Third, you build the HNSW graph and query it with a vector created from the user input. The library returns nearest-neighbor IDs and distances, and your app decides how to display or post-process them.
Because the retrieval step is approximate nearest-neighbor search, it stays fast even as the dataset grows beyond trivial linear scans. The most important quality lever is still the embedding model. Better vectors usually matter more than micro-optimizing ANN parameters, so teams should benchmark retrieval quality on real user phrasing before shipping the experience widely.
When to use this pattern
This is a practical fit when the search corpus is small to medium, shipped with the app, and searched frequently enough that backend latency would be noticeable. Common examples include docs portals, embedded support widgets, local-first assistants, and curated catalogs.
- Small to medium storefronts
- Shoppable landing pages
- Embedded search widgets
- Trade-show or kiosk catalogs
Limitations
Client-side search is best when the corpus is safe to ship and moderate in size. Large catalogs, strict ACLs, and real-time inventory writes usually push you toward a server-side index or hybrid architecture.
Be especially careful about corpus size, update frequency, and data sensitivity. Browser vector search is excellent when those three constraints are favorable, but it is not the right answer when the dataset is huge, private, or changing constantly for every user.
FAQ
Can altor-vec replace Algolia or Elasticsearch for every store?
No. It is strongest for smaller, shippable catalogs and local UX. Large stores usually need server-side indexing, faceting, and operational tooling.
Can I combine semantic search with filters?
Yes. A common pattern is semantic retrieval first, followed by metadata filters such as brand, stock status, or price range.
Why use vector search for products instead of keyword search only?
Vectors help match intent, synonyms, and descriptive phrasing that exact-term matching often misses.
Get started: npm install altor-vec · GitHub