Embeddings explained for web developers
Embeddings explained for web developers: what the vectors are, how cosine similarity works, and practical uses in search, dedupe and recommendations.
By Hamza Ghouri
Embeddings are the least flashy part of the AI stack and, for a lot of products, the most useful. If you build web apps, you can think of an embedding as a fingerprint for meaning: a list of numbers that places a piece of text (or an image) in a space where similar things sit close together. Once you have those numbers, a whole set of features becomes a database query. This post explains embeddings for web developers without the maths lecture, and covers where I'd use them beyond chatbots.
What an embedding actually is
You send a string to an embedding endpoint and get back an array of floats, typically a few hundred to a few thousand of them. That array is a point in a high-dimensional space. The model that produced it was trained so that texts with similar meaning land near each other.
So "How do I reset my password?" and "I forgot my login" end up close together, even though they share no words. "Reset the router" ends up somewhere else. Keyword search sees two unrelated strings in the first pair. Embeddings see the same intent.
A few properties are worth knowing up front:
- Vectors from different models are not comparable. An embedding only means something next to other embeddings from the same model. Switching models means re-embedding everything.
- Dimension is fixed per model. Your database column has to match it.
- Embeddings are cheap compared with text generation. You can embed your whole catalogue once and store the results.
- They are not reversible in any practical sense, but they are still derived from your data. Treat stored embeddings with the same privacy rules as the source text.
Measuring similarity
"Close together" needs a number. The usual choice is cosine similarity: the cosine of the angle between two vectors. It ranges from -1 to 1, and for text embeddings higher means more similar. Here is the whole thing in TypeScript:
export function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) throw new Error("Dimension mismatch");
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
That function is fine for comparing a handful of vectors in memory. For thousands or millions, you want an index that finds nearest neighbours without comparing against every row. If you run Postgres, the pgvector extension does exactly that, and I walk through the setup in vector search in PostgreSQL with pgvector.
One practical warning: similarity scores are relative. A score of 0.8 might be a strong match with one model and a weak one with another. Use the scores to rank results, and set any cut-off thresholds by looking at real examples from your own data.
Use cases beyond chat
Most people meet embeddings through retrieval-augmented generation, where they find relevant documents to feed a language model. That is a great use, and RAG in Node.js covers it end to end. But plenty of valuable features need no text generation at all.
Semantic search
Embed every item in your catalogue (products, articles, help pages). Embed the user's query. Return the nearest items. Users who type "something to keep coffee hot" find the thermos listed as "insulated travel mug". This is the most direct upgrade you can make to a search box, and it pairs well with keyword search for exact terms like SKUs.
Deduplication
Duplicate records are a quiet tax on every marketplace and CRM: the same product listed twice with slightly different titles, the same support ticket filed three times. Embed each record and flag pairs above a similarity threshold for review. It catches rewordings that exact matching and fuzzy string matching miss.
Recommendations
"More like this" is a nearest-neighbour query on the item the user is looking at. For a cold-start product with no interaction data yet, content-based recommendations from embeddings are often the only option that works on day one. You can also average the embeddings of items a user has engaged with to build a rough taste profile.
Classification and routing
Embed a few labelled examples per category (billing, bug report, feature request). For a new message, find which category's examples it sits closest to. This is a surprisingly strong baseline for ticket routing and tagging, and it is far cheaper and faster than calling a generative model for every message.
Clustering
Group thousands of free-text survey answers or reviews by embedding them and running a standard clustering algorithm. You get themes without reading every response, and then a person can name and verify the clusters.
Practical tips for web apps
- Embed at write time. Generate the embedding when a record is created or updated, in a background job, and store it with the row. Query-time embedding should only happen for the user's input.
- Embed the right text. For a product, a combination of title, category and description usually beats the title alone. Be deliberate about what goes in.
- Hash inputs. Store a hash of the embedded text so you only re-embed when it changes.
- Cache query embeddings. Popular searches repeat. A short-lived cache keyed on the normalised query saves calls, the same idea as caching LLM responses with Redis.
- Record the model. Store which embedding model produced each vector, so a future migration is a clean backfill.
The short version
An embedding turns meaning into numbers, and cosine similarity turns "is this like that?" into a query. Start with one feature that needs no generation at all (semantic search or "more like this"), store the vectors in the database you already run, and rank by similarity. It is one of the cheapest ways to add genuinely useful AI to a web product.