An embedding turns a piece of text or an image into a list of numbers, so "close in meaning" becomes "close in distance" and your database can sort by it. That buys four things worth building on: search that survives the words people type, near-duplicate detection, recommendations before you have click data, and classification when the label set keeps moving. Everything else people build with embeddings is usually a keyword index in a costume. Start with pgvector in the database you already run.
What a vector embedding is, in plain terms
A vector embedding is a fixed-length list of numbers that stands in for meaning. A model reads a product description, a support ticket or an image and returns an array of floats, commonly a few hundred to a few thousand of them. Two pieces of content about the same thing land near each other in that space even when they share no words at all.
The half that matters for product work is that nearness is measurable. Cosine distance (the angle between two of those arrays, ignoring how long they are) gives you a single number, and a number is something a database can put in an ORDER BY. A problem that used to need a human guessing synonyms becomes a sort.
The model you pick matters less than teams expect. Retrieval fails on plumbing: stale vectors, no filter in the query, and ranking by distance alone as though distance knew about stock levels.
Where embeddings earn their place
Four product problems, and they have a shape in common: the user's words and your data's words do not match, and no amount of indexing fixes that.
Search that survives the words people type
Someone types "waterproof speaker for the shower". Your catalogue says "IPX7 portable Bluetooth speaker". No LIKE clause and no full-text index connects those two, because they share one word and it is the wrong one.
Embeddings bridge that gap. The strongest setup is hybrid: keyword search for the exact matches it is unbeatable at, vector search for intent, then one ranking pass over both. A pure vector search will happily miss a SKU typed in full.
When the problem is the number of filters rather than the wording, that is a different fix, and I wrote about hitting that wall in the query that broke our MySQL search.
Near-duplicate detection
This is the quiet win nobody blogs about. Two suppliers upload the same product with different titles. A user opens a ticket that is the same issue as the one from this morning, worded differently. A content team pastes a variant of a page that already exists.
Exact matching finds none of those, and fuzzy string matching finds some while lying about the rest. Distance finds them, and you tune one threshold rather than a pile of rules.
Recommendations before you have behaviour data
Content similarity works on day one. Collaborative filtering, the "people who bought this also bought" approach, needs traffic you do not have yet and is useless for a product added this morning.
Embedding the title and description gives you "more like this" from the first row, and it keeps working for the long tail that never gets enough clicks to rank.
Classification when the labels keep moving
Support triage, tagging, routing. A trained classifier is the textbook answer and a maintenance problem, because every new label is a retraining job.
Store a few example embeddings per label instead and classify by whichever label's examples sit nearest. Adding a category becomes adding three examples, which a support lead can do without an engineer.
Where a plain index still wins
If the query is an identifier or a filter, embeddings are the wrong tool. SKU, order id, invoice number, exact product name, email address: an index answers those faster and cheaper, and it answers them exactly. Similarity search returns the nearest thing whether or not the right thing exists, which is precisely the behaviour you do not want when a customer pastes an order number.
The same goes for structured filtering. Price ranges, categories, stock, date windows: that is what a relational index is for, and the fix when it gets slow is usually the layout of the data rather than a model. On one catalogue the answer to a 300 second filter query was a denormalized lookup table, and no embedding would have helped.
Three more limits worth knowing before you commit:
- A similarity signal is not a source of truth. Ordering purely by distance ships a page of out-of-stock, wrong-region results. Distance is one input to ranking.
- They cost money twice, once per item embedded and once per query, and the second one is on your hot path.
- They go stale. Two models do not share a coordinate space, so changing model invalidates every vector you hold. Re-embedding a catalogue is a migration with a dual-write window, not an afternoon.
Start with pgvector next to your rows
Put the vector in the table it describes, in the database you already run. pgvector adds a vector column type to Postgres along with distance operators and two index types, and for the overwhelming majority of products that is the end of the decision.
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE products ADD COLUMN embedding vector(1536);
CREATE INDEX ON products USING hnsw (embedding vector_cosine_ops);
SELECT id, title
FROM products
WHERE tenant_id = $1 AND in_stock
ORDER BY embedding <=> $2
LIMIT 20;<=> is cosine distance, <-> is L2, and each operator needs an index built with its matching operator class, so pick the one your model recommends and build for that one. HNSW builds slower and answers faster than IVFFlat, which is the right default when reads outnumber writes. An indexed vector column tops out at 2000 dimensions; halfvec doubles that ceiling at half precision, which covers the larger models.
On a Laravel codebase the package wraps the same thing, so a migration and a query stay in Eloquent:
use Pgvector\Laravel\Vector;
use Pgvector\Laravel\Distance;
class Product extends Model
{
use \Pgvector\Laravel\HasNeighbors;
protected $casts = ['embedding' => Vector::class];
}
// $table->vector('embedding', 1536); in the migration
$similar = $product->nearestNeighbors('embedding', Distance::Cosine)
->where('in_stock', true)
->take(10)
->get();Look at the WHERE clause in both, because that is the real argument. Your permissions, your tenant scope and your stock flag live in the same query as the similarity sort. A separate vector database means two systems, two consistency stories, and filtering that happens in your application after the fact.
One caveat to test rather than assume: a very selective filter plus an approximate index can return fewer rows than you asked for, because the index narrows the search before your WHERE clause runs. Recent pgvector versions have options for this, so check yours before designing around it.
On MySQL, read your version notes closely. A VECTOR column type exists in recent MySQL, but "the type exists" is a long way from "the search is indexed".
The signal you have outgrown pgvector
It is latency under real concurrency with your real filters, not row count. Teams move too early because they read a benchmark with a million rows in it and no WHERE clause. Benchmark your query, with your filters, at your concurrency.
The honest triggers: index builds that no longer fit the maintenance window, a working set too large for memory on a database you also need for transactions, or a ranking pipeline where you are hand-rolling features a specialist engine ships.
Until one of those is true, the second database is pure cost: another deployment, another backup story, another place for your data to disagree with itself.
Reach for embeddings when the user's words and your data's words diverge. Reach for an index when they do not. Most products need both, and the mistake I see most often is shipping a model where a WHERE clause would have done.
FAQ
Do I need a vector database to use embeddings?
No. Postgres with pgvector handles similarity search for most products, and keeping vectors beside the rows they describe lets one query filter and rank together. Move to a dedicated engine when latency under your real concurrency and filters degrades, not at a row count.
Are embeddings a replacement for full-text search?
No. Keyword search wins on exact terms, identifiers and rare words, while embeddings win on intent and phrasing. Run both and merge the results, because each fails where the other is strongest.
How often do I need to re-embed my content?
Whenever the content changes, and completely whenever the model changes. Vectors from two models are not comparable, so a model upgrade is a full re-embed and should be planned as a migration with a dual-write window.


