E-commerce Vector Search Architecture: Key Takeaways
- A vector search architecture has four layers: source data, synchronization and embedding, a search index, and a query path.
- Commerce platforms and PIMs supply product facts, while the headless CMS supplies editorial content. Search records combine the fields shoppers need to find, filter and evaluate products.
- Hybrid search combines keyword matching with vector similarity, supporting both exact product identifiers and descriptive queries. How you combine the two determines the trade-off between relevance and speed.
- Algolia, Pinecone and pgvector offer different levels of control over indexing, retrieval and operations. The right choice depends on how much of the search pipeline your team wants to own.
A shopper searches for “breathable sheets for hot summer nights” and gets pillowcases—or nothing. Fixing that experience takes more than choosing a vector database. The search system needs complete product records, reliable updates, and a retrieval strategy that handles both descriptive requests and exact product identifiers.
Ecommerce search connects a shopper’s query with the right products, but relevance is only part of the job. Results also need accurate prices, current availability and filters that reflect the catalog. On a headless stack, that means bringing commerce data and CMS content into a reliable search index, then combining exact keyword matches with semantic retrieval.
We have built search on very different stacks: a conference video portal where every word spoken on stage is searchable, a Strapi-based trend platform that vectorizes new content every night, and a 24,000-SKU Shopify Hydrogen store using Sanity for content and Algolia for instant search. If you lead engineering or own the search budget for a headless store, this guide explains the architecture those builds taught us, layer by layer, and where Algolia, Pinecone and pgvector fit.
What is vector search and how does it work?
Vector search finds items by meaning rather than relying only on shared words. An embedding model turns text, such as a product title and description, into a vector: a list of numbers representing its meaning. OpenAI’s
text-embedding-3-small, for example, produces vectors with 1,536 dimensions by default. Texts with similar meanings can land close together, allowing “breathable sheets” to match “lightweight linen bedding” even though the phrases share no words.
At query time the shopper’s text goes through the same model, and the engine returns the products whose vectors sit closest to the query vector. Comparing the query with every product is exact but gets slow on a large catalog, so vector search algorithms build an approximate nearest neighbor (ANN) index: a shortcut structure that gives up a little accuracy for a large gain in speed. The most common one is HNSW (Hierarchical Navigable Small World). “pgvector supports HNSW, and most vector databases use it by default.”, and it is the default dense-vector index in open-source engines such as Qdrant and Weaviate.
What’s the difference between semantic search and vector search?
Vector search and semantic search are often used as synonyms, but they sit at different levels. Semantic search, sometimes called contextual semantic search, is the goal: results that match what the shopper meant. Vector search is the most common mechanism for getting there. A semantic search engine can also rely on query rewriting, synonym lists or a language model, and a vector index can power features that are not search at all, such as “similar products” blocks.
Matching by meaning has its own failure mode. On our conference video portal, the query “great react” returned talks about audience reactions, because two words gave the model too little context to know React.js was meant. Product search runs into the same problem with short queries, SKUs, sizes and brand names. That is why catalogs rarely run on vectors alone.
| Keyword search | Vector search | Hybrid search | |
|---|---|---|---|
| Matches on | exact terms, typos, configured synonyms | meaning of the whole query | both, with scores blended |
| Strong at | SKUs, brand names, sizes | descriptive queries (“gift for a runner”) | mixed catalogs |
| Weak at | wording the catalog does not use | short or exact-term queries | someone has to balance keyword and meaning results |
| Typical engines | Algolia keyword ranking, Postgres full-text, Elasticsearch | pgvector, Pinecone | Algolia NeuralSearch, Postgres full-text + pgvector |
Verdict under the vendor table
Which one should you choose?
- Choose Algolia if your merchandising team needs to control results itself, with rules and pinned products, and you don’t want to run an embedding pipeline. Check that your plan includes NeuralSearch before you commit.
- Choose Pinecone if you want vector search as a separate managed service, your backend can already look up products by ID, and your engineers can tune the balance between keyword and vector results.
- Choose pgvector if your commerce platform or CMS already runs on Postgres, such as Medusa or Payload with its Postgres adapter, and you’d rather keep one database than sync to an external service. You write and maintain the hybrid query in SQL.
None of the three will fix a weak sync layer. A good engine only ranks the records it’s given.
Why keyword ecommerce site search breaks on real catalogs
Keyword search works well when shoppers use the catalog’s vocabulary: a product name, brand or SKU. It becomes less reliable when they describe a need in different words, such as “something warm for a guest bed” or “sheets that don’t wrinkle.” Synonyms and query expansion can bridge some of these gaps, but they require configuration and ongoing maintenance across the store’s languages.
It breaks when they describe a need, such as “something warm for a guest bed” or “sheets that don’t wrinkle”, because no product title contains those phrases. Synonym lists patch the most common gaps, but content writers/managers have to write and maintain them for every language the store sells in.
Real catalogs make the gap wider. Caleffi, an Italian textile brand we moved from Magento to Shopify Hydrogen with Sanity, sells 24,000+ SKUs in four languages. Its descriptive attributes, such as fabric, composition, color and season, come from a PIM and were stored in Italian only. A German shopper who types ‘Baumwolle’ gets nothing back from fields that say ‘cotone’.
Multilingual embedding models solve this by placing text in different languages in one vector space. According to OpenAI reports, text-embedding-3-small scores 44.0 on the MIRACL multilingual retrieval benchmark, up from 31.4 for text-embedding-ada-002. Test it on your own catalog languages before relying on it.
Keyword matching still earns its place for part of a store’s internal search traffic. Shoppers who type a SKU, a brand or an exact product name expect an exact hit, and B2B buyers reordering from a known catalog often search that way. For most stores the practical question is how to combine the two methods.
Vector search architecture for an e-commerce catalog, layer by layer
On paper the four layers look simple. Each one hides a decision:
- Source: which system owns which fields. Shopify Admin or a PIM holds price, stock and variants, while the headless CMS holds descriptions, categories and translations.
- Sync and embedding: what triggers an update, a webhook (an automatic notification sent when content changes) or a nightly job, and which fields are worth re-embedding, since every embedding is a paid API call.
- Index: a hosted engine like Algolia, a dedicated vector database like Pinecone, or pgvector inside the Postgres you already run.
- Query: how the shopper’s text becomes a vector fast enough for search-as-you-type, using the same model as at indexing time, because vectors from different models cannot be compared.
The choice of ecommerce product search engine changes layer 3. Layers 1 and 2 decide whether results are fresh, complete and affordable, and they take most of the engineering on our headless e-commerce builds that include search, because a mistake there shows up later as a relevance problem that no ranking setting can fix.
Where should the search record be built?
Most reference diagrams put the headless CMS at the top as the single source of truth for the catalog. On composable stacks, that is rarely true. On Caleffi’s Hydrogen store, product data is split across three systems:
- Shopify Admin owns titles, variants, prices and stock.
- The PIM adds attributes such as fabric, composition, color and season.
- Sanity holds editorial copy, category structure and translations.
Shopify sends product data to Sanity through webhooks, so Sanity ends up holding both the product facts and the editorial content.
A search record needs both halves too. Our rule is to build it in the system where they already meet. When Shopify data is synced into Sanity, through Sanity Connect or a custom sync like Caleffi’s, a single Sanity webhook sees every change that matters to search. Medusa runs on Postgres, and Payload can run on Postgres through its adapter. When products and content live in the same database, the search record can be assembled there without first copying both into another service. Field ownership still needs to be explicit.
Should the index update on every change or on a schedule?
Webhooks can keep the index fresh within seconds, but a naive implementation may request a new embedding on every save, even when only the price or stock changed. On Future Possible, a trend-tracking platform on Strapi, we went the other way: a Vercel cron job syncs, vectorizes and indexes new CMS content every night at 03:00 UTC. For a library of 2,000+ trend entries, a nightly batch is simpler to run and to retry than a stream of events.
For a catalog, our answer is both, split by what changes. Price and stock change constantly and must reach the index within seconds. But they don’t change what the product means, so they don’t need a new embedding. They’re stored as metadata: plain fields next to the vector, used for filtering and display.
Descriptions and attributes change rarely, and they are the only fields worth re-embedding, whether on each change or in a nightly batch. The fast path for price and stock has to exist either way: a result that shows the wrong price or an out-of-stock product costs more trust than a weaker match.
What text should go into the embedding?
Embed the words a shopper might use: title, product type, brand, tags and the editorial description. Leave out price, stock and internal IDs, which add noise to the vector and belong in filters. To avoid paying for a new embedding when only the price changes, store a hash, a short fingerprint of the embedded text, next to the vector and skip the call when it matches. Include the model name in that hash, so a model change triggers re-embedding of the whole catalog.
The embedding bill itself is small. At OpenAI’s list price of $0.02 per million tokens for text-embedding-3-small, a 24,000-product catalog at roughly 500 tokens of text per product costs about 25 cents to embed in full, by our estimate. The real cost of this layer is engineering time.
How should drafts and deleted products leave the index?
Sanity webhooks do not fire on drafts by default, which keeps unreviewed copy out of the index. Deletions need more care. When a product is removed in Shopify, Sanity Connect keeps the document and sets store.isDeleted to true. The sync has to remove the product from the index in all three cases: the isDeleted flag, an archived status, or a real delete.
Here is the whole sync layer for a Shopify and Sanity store, as a Next.js route handler on Vercel that writes to Pinecone, which keeps the example short; the same logic works with pgvector or the Algolia API. In plain terms: when a product changes, the handler removes it from the index if it is deleted or not active, refreshes only the metadata if the descriptive text is unchanged, and pays for a new embedding only when the text itself changed.
// app/api/search-sync/route.ts
import { createHash } from 'node:crypto';
import { isValidSignature, SIGNATURE_HEADER_NAME } from '@sanity/webhook';
import { Pinecone } from '@pinecone-database/pinecone';
import OpenAI from 'openai';
// Sanity GROQ webhook, filter: _type == "product", projection:
// { _id, "title": store.title, "status": store.status, "productType": store.productType,
// "vendor": store.vendor, "tags": store.tags, "price": store.priceRange.minVariantPrice,
// "slug": store.slug.current, "isDeleted": store.isDeleted, body }
type Block = { _type: string; children?: { text?: string }[] };
type ProductPayload = {
_id: string;
title?: string;
status?: string; // active | archived | draft | ...
productType?: string;
vendor?: string;
tags?: string | string[]; // Sanity Connect stores a string, custom syncs often an array
price?: number;
slug?: string;
isDeleted?: boolean; // Sanity Connect flags Shopify deletions instead of deleting the document
body?: Block[];
};
const EMBEDDING_MODEL = 'text-embedding-3-small';
const openai = new OpenAI(); // OPENAI_API_KEY
const index = new Pinecone().index({ name: 'products' }); // PINECONE_API_KEY
// Editorial copy written in Sanity, flattened to plain text.
function blocksToText(blocks: Block[] = []): string {
return blocks
.filter((block) => block._type === 'block')
.map((block) => block.children?.map((child) => child.text ?? '').join('') ?? '')
.join('\n');
}
// Only words that describe the product. Price and stock go to metadata, never into the vector.
function textForEmbedding(product: ProductPayload): string {
const tags = Array.isArray(product.tags) ? product.tags.join(', ') : product.tags;
return [product.title, product.productType, product.vendor, tags, blocksToText(product.body)]
.filter(Boolean)
.join('\n');
}
export async function POST(request: Request): Promise<Response> {
const rawBody = await request.text();
const signature = request.headers.get(SIGNATURE_HEADER_NAME) ?? '';
const secret = process.env.SANITY_WEBHOOK_SECRET;
if (!secret || !(await isValidSignature(rawBody, signature, secret))) {
return new Response('Invalid signature', { status: 401 });
}
const product = JSON.parse(rawBody) as ProductPayload;
const operation = request.headers.get('sanity-operation'); // create | update | delete
const id = product._id;
if (operation === 'delete' || product.isDeleted || product.status !== 'active') {
await index.deleteOne({ id });
return Response.json({ id, action: 'removed' });
}
const text = textForEmbedding(product);
const hash = createHash('sha256').update(`${EMBEDDING_MODEL}:${text}`).digest('hex');
const metadata = { title: product.title ?? '', slug: product.slug ?? '', price: product.price ?? 0, hash };
const { records } = await index.fetch({ ids: [id] });
if (records[id]?.metadata?.hash === hash) {
// Same text, same model: refresh price and slug without paying for a new embedding.
await index.update({ id, metadata });
return Response.json({ id, action: 'metadata-only' });
}
const { data } = await openai.embeddings.create({ model: EMBEDDING_MODEL, input: text });
await index.upsert({ records: [{ id, values: data[0].embedding, metadata }] });
return Response.json({ id, action: 'embedded' });
}
The same layer looks different on each stack, but the decisions stay the same:
| Stack | What triggers the sync | Where vectors usually live | What to watch |
|---|---|---|---|
| Shopify + Sanity | Sanity webhook on the product document, with Shopify data synced in | Algolia or Pinecone | lag between a Shopify change and its copy in Sanity |
| Payload on Postgres | collection afterChange and afterDelete hooks |
pgvector in the same database | embedding calls slowing down the editor’s save; queue them |
| Medusa v2 | subscriber on product.updated and product.deleted |
pgvector or an external index | one record per product or per variant, which decides whether each color shows as its own result |
| Strapi | lifecycle hooks or a scheduled job | pgvector or an external index | REST query limits on large reads; direct SQL helped us |
Algolia, Pinecone or pgvector: choosing a vector search database
The best ecommerce search engine for a catalog depends on how much of the pipeline you want to hand off, and the three options differ exactly there. Algolia NeuralSearch adds vector search to Algolia’s keyword search, then merges and ranks both result sets in one query. Algolia builds the vectors from your record attributes itself, so your sync layer sends ordinary records and never touches an embedding model. The catch is availability: NeuralSearch is not included in every Algolia plan.
Pinecone is a dedicated vector database. You send it vectors, or plain text if the index uses integrated embedding, where Pinecone runs the model for you, and a query returns matching IDs with scores and whatever metadata you stored. The storefront still needs product cards, so you either keep display fields in metadata or fetch them from your commerce backend after the query. Hybrid search is available too, for example as dense and sparse vectors on the same record or as a keyword filter before the vector ranking. Tuning the balance between the two is your team’s job.
pgvector is a Postgres extension, so the vectors live in the same database as the content. On Medusa, or on Payload with its Postgres adapter, that removes the sync to an external service entirely. Hybrid search becomes one SQL query that combines Postgres full-text search with vector similarity, which is how we built search on our conference portal, covered in the next section.
| Algolia with NeuralSearch | Pinecone | pgvector | |
|---|---|---|---|
| Who builds the embeddings | Algolia | your sync layer, or Pinecone’s integrated embedding | your sync layer |
| Keyword + vector in one query | yes, merged and ranked by Algolia | yes, for example dense + sparse vectors or a keyword filter | yes, full-text search and vectors in one SQL query |
| What a query returns | full records, ready to render | IDs, scores, stored metadata | rows from your own tables |
| Tools for the merchandising team | Rules, pinned results, Merchandising Studio on higher plans | none built in | none built in |
| Extra service to run | hosted, no infrastructure | hosted, no infrastructure | none if you already run Postgres |
Hybrid search on the query path: from 6 seconds to 500 ms
What our conference search optimization taught us, and what changes for catalogs.
With Algolia, the shopper’s browser talks to the search engine directly, using a search-only API key that is safe to expose in frontend code. With Pinecone or pgvector, every search makes an extra stop at your server: the keys must stay secret, and the query has to be turned into a vector before the index can be asked. That extra call to the embedding model sits in front of every search.
On our conference portal, keyword prefiltering reduced the rows passed to semantic ranking and cut query execution time from about 6 seconds to about 500 ms. Our hybrid search walkthrough with OpenAI and pgvector explains the implementation, including the SQL behind the optimization.
For e-commerce catalogs, the same approach has an important trade-off: products with no keyword overlap never reach semantic ranking, even if they match the shopper’s intent. Where semantic discovery matters, consider retrieving keyword and vector candidates separately, then merging and reranking the results. Keep explicit constraints, such as the shopper’s selected category, size or availability, separate from keyword matching so they narrow eligible products without requiring the shopper to use the catalog’s exact wording.
How do you know if your vector search works?
Measure what the shopper from the opening example would experience: a search that returns nothing useful and a visit that ends there. Three metrics show it:
- Zero-result rate: the share of searches that return no products. Hybrid search should cut it sharply on descriptive queries.
- Search exit rate: the share of shoppers who leave the store straight from the results page. It catches searches that return results, just the wrong ones, which zero-result rate misses.
- Search-to-cart rate: the share of search sessions that end with an add-to-cart. It’s the metric that ties relevance to revenue.
Algolia reports no-result searches and click positions in its analytics. With Pinecone or pgvector, you log queries and results yourself. GA4’s enhanced measurement can capture the search terms on either stack.
To compare keyword and hybrid search fairly, build a test set from your own search logs. Include your top queries, the queries that return zero results today, long descriptive queries such as “breathable sheets for hot summer nights”, and exact-term queries such as SKUs and brand names. Run the same set through both setups and check the top five results for each query. Then A/B test the winner on live traffic. Keep the test set, too: when you switch embedding models, rerunning it shows whether the upgrade helped before shoppers notice.
Where vector search on a headless stack really gets decided
On the builds behind this guide, search quality was settled before any vendor demo: by which system owned each field, how changes reached the index, and whether keyword matching stayed for the queries that need exact hits. With those in place, Algolia, Pinecone and pgvector can each carry a catalog well. Without them, a better engine only hides the gaps for a while.
Embedding models will keep changing, and engines will keep moving hybrid ranking into their defaults. What lasts is the sync layer in the middle, the one that knows where product facts and editorial content live. Build it so that a new model means re-processing the catalog in the background with no change to the storefront, and every shift in the AI market becomes a routine upgrade.
If catalog search is the part of your stack that doesn’t hold up, let’s talk. We’ll tell you which architecture we’d pick for your catalog and why.