Have you ever searched for something online and got the right result even though you didn’t type the exact words?
That’s semantic search in action, and behind it lies one of the most fascinating concepts in AI: embeddings.
In this article, we’ll explore what embeddings are, why they’re helpful for search and similarity tasks, and how to experiment with them directly using JavaScript, LangChain, and Ollama.
We’ll even see how words like “Christmas” and “December festivity” can be recognized as semantically related, using just a few lines of code.
When embeddings are useful
Traditional search engines rely on keywords, looking for exact matches.
Embeddings go further: they let computers understand meaning.
Imagine searching for “holiday celebrations in winter” and finding results about “Christmas traditions” or “New Year’s Eve parties.”
That’s not keyword matching, that’s semantic similarity.
Embeddings make this possible by converting text (words, phrases, or sentences) into numeric vectors, which are lists of numbers that capture meaning.
How embeddings capture meaning
Think of embeddings as coordinates on a map of meaning.
Each word or sentence becomes a point in a high-dimensional space.
Words that appear in similar contexts, like “Christmas” and “December festivity”, end up near each other on that map.
Why?
Because the AI model that produces these embeddings has seen billions of examples during training and has learned which words tend to appear together or in similar situations.
If two terms are used in similar sentences and share context, their vectors will be numerically close.
When compared using a distance metric such as cosine similarity, these vectors yield high similarity scores.
In short:
Words that mean similar things have "close" embeddings.
Simplifying the example
To make things easier, we’ll start simple, comparing individual words.
In practice, embeddings are even more effective with complete sentences or product descriptions, as longer text provides richer context and disambiguates meaning (for example, “Apple” as a fruit vs. “Apple” as a company).
But for our demo, we’ll stick to words like “Christmas,” “New Year,” and “Banana”, and see how a model measures their semantic similarity.
Bringing it to life with JavaScript, LangChain, and Ollama
Now that we’ve covered what embeddings are and why they matter, let’s see them in action.
For this hands-on example, we’ll use JavaScript, one of the most accessible and widely used languages among web developers.
We’ll combine two powerful tools:
- LangChain.js: a modern framework that lets you integrate AI capabilities (like embeddings, LLMs, and retrieval) into your JavaScript applications.
- Ollama: a lightweight, local AI runtime that runs models directly on your machine, so you don’t need any cloud APIs or keys.
By pairing these two, we can generate embeddings locally, compare their semantic similarity, and visualize how close words are in “meaning space”, all from a simple JavaScript logic.
In our example, we’ll take a small list of words like:
["Christmas", "December festivity", "New Year", "Easter", "Car", "Bicycle", "Banana"]
…and measure how close each one is to “Christmas.”
You’ll quickly see how embeddings help models recognize that “December festivity” is far more related to “Christmas” than “Banana” ever could be.
Setting up LangChain + Ollama
We’ll use LangChain.js with Ollama, a local model runner that supports embedding models like nomic-embed-text.
Installation
Make sure you have Bun (or Node if you prefer it), then:
bun add @langchain/ollama @langchain/core
Start Ollama locally and pull the embedding model:
ollama serve
ollama pull nomic-embed-text
The code: embeddings and similarity in JavaScript
Here’s the full working example.
// embeddings-demo.js
import { OllamaEmbeddings } from "@langchain/ollama";
// --- Configuration ---
// Make sure Ollama is running locally and the embedding model is available
// Example: ollama pull nomic-embed-text
const embeddings = new OllamaEmbeddings({
model: "nomic-embed-text",
baseUrl: "http://localhost:11434", // Ollama API URL
});
const referenceWord = "Christmas";
const wordList = [
"Christmas",
"December festivity",
"New Year",
"Easter",
"Car",
"Bicycle",
"Banana",
];
function cosine(a, b) {
let p = 0;
let p2 = 0;
let q2 = 0;
for (let i = 0; i < a.length; i++) {
p += a[i] * b[i];
p2 += a[i] * a[i];
q2 += b[i] * b[i];
}
return p / (Math.sqrt(p2) * Math.sqrt(q2));
}
console.log("Generating embeddings...");
// Get reference embedding
const referenceEmbedding = await embeddings.embedQuery(referenceWord);
// Get embeddings for all words
const embeds = await embeddings.embedDocuments(wordList);
console.log("\nVector Similarities:\n");
let idx = 0;
for (const word of wordList) {
const similarity = cosine(referenceEmbedding, embeds[idx++]);
console.log(`sim(${referenceWord}, ${word}) = ${similarity}`);
}
Step-by-Step code explanation
1. Create the embedding provider
We initialize OllamaEmbeddings with our model and API URL.
const embeddings = new OllamaEmbeddings({
model: "nomic-embed-text",
baseUrl: "http://localhost:11434",
});
This tells LangChain to connect to your local Ollama instance and use the specified model to generate embeddings.
2. Prepare your data
We define a reference word and a list of other words we’ll compare it to.
const referenceWord = "Christmas";
const wordList = ["Christmas", "December festivity", "New Year", "Easter", "Car", "Bicycle", "Banana"];
3. Generate embeddings
We ask the model to convert each word into its numerical representation (a vector).
// Get reference embedding
const referenceEmbedding = await embeddings.embedQuery(referenceWord);
// Get embeddings for all words
const embeds = await embeddings.embedDocuments(wordList);
Each embedding is an array of floats, like [0.124, -0.087, 0.993, ...].
4. Compute similarity
The cosine similarity measures how close two vectors are in direction.
The closer to 1.0, the more similar their meanings.
function cosine(a, b) {
...
return p / (Math.sqrt(p2) * Math.sqrt(q2));
}
5. Print the results
Running bun run embeddings-demo.js outputs something like:
Generating embeddings...
Vector Similarities:
sim(Christmas, Christmas) = 0.9999999999999998
sim(Christmas, December festivity) = 0.8004442006023177
sim(Christmas, New Year) = 0.6254366870703798
sim(Christmas, Easter) = 0.6475123420292588
sim(Christmas, Car) = 0.4313590955452999
sim(Christmas, Bicycle) = 0.3869358120850754
sim(Christmas, Banana) = 0.41243323374530955
Notice how “Christmas” and “December festivity” are much closer in meaning than “Christmas” and “Banana”.
Where word embeddings can fall short
Word embeddings work well for simple comparisons, but they don’t always capture the full context.
For example, “bank” (riverbank vs. financial institution) or Apple (company vs fruit) has multiple meanings.
Sentence-level embeddings handle this better because they consider surrounding words and relationships.
That’s why modern applications use sentence or document embeddings to understand full ideas, not isolated words.
Recap and next steps
You’ve just built a mini semantic similarity engine using:
- Embeddings to represent meaning
- Cosine similarity to measure closeness
- LangChain + Ollama for local AI processing
Now that you understand the basics, try experimenting with:
- Longer phrases or full sentences
- Product or article descriptions for semantic search
- Combining embeddings with a vector database (like Chroma or Pinecore)
Embeddings unlock a whole new way of thinking about search, clustering, and recommendations by focusing on meaning rather than keywords.
Top comments (0)