Embeddings & Vector Search with JavaScript
Traditional search in databases looks for exact keyword matches. If a user searches for "cheap flights", standard SQL LIKE '%cheap flights%' misses articles containing "affordable airfare" or "budget airlines".
Vector search solves this problem by searching for content based on meaning (semantics) rather than matching characters.
1. Keyword Search vs. Semantic Search
2. What is a Text Embedding?
An embedding is an array of floating-point numbers (a mathematical vector) that represents the semantic meaning of a piece of text.
"Happy puppy" ───> Embedding Model ───> [ 0.23, -0.45, 0.12, 0.89, ... 1536 dimensions ]
"Joyful dog" ───> Embedding Model ───> [ 0.22, -0.44, 0.11, 0.88, ... 1536 dimensions ]
"Database index" ───> Embedding Model ───> [-0.85, 0.12, 0.64, -0.32, ... 1536 dimensions ]Notice how "Happy puppy" and "Joyful dog" produce vectors that are numerically very close to each other in vector space, while "Database index" is far away.
3. Generating Embeddings in Node.js
Let's use OpenAI's text-embedding-3-small model to generate embeddings in JavaScript:
import OpenAI from "openai";
import dotenv from "dotenv";
dotenv.config();
const openai = new OpenAI();
async function getEmbedding(text) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text
});
return response.data[0].embedding;
}
// Test generating a vector
async function run() {
const vector = await getEmbedding("How do I update my billing email?");
console.log(`Vector Dimensions: ${vector.length}`); // 1536
console.log(`First 5 numbers:`, vector.slice(0, 5));
}
run();| Embedding Model | Dimensions | Price per 1M Tokens | Best Use Case |
|---|---|---|---|
text-embedding-3-small | 1536 | $0.02 | Recommended default for all web & backend apps |
text-embedding-3-large | 3072 | $0.13 | High-precision scientific or legal retrieval |
4. Setting Up pgvector in PostgreSQL
Instead of managing an entirely separate vector database, you can store and query vectors directly inside PostgreSQL using the official pgvector extension.
SQL Setup Script
-- 1. Enable the vector extension in your Postgres database
CREATE EXTENSION IF NOT EXISTS vector;
-- 2. Create your documents table with a vector column
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1536),
created_at TIMESTAMP DEFAULT NOW()
);
-- 3. Create an IVFFlat index for lightning-fast approximate nearest neighbor search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);5. Storing and Searching Embeddings with Node.js (pg)
Install the PostgreSQL client library:
npm install pgHere is how to connect, insert documents with their vectors, and perform cosine similarity searches:
import pg from "pg";
import OpenAI from "openai";
import dotenv from "dotenv";
dotenv.config();
const { Pool } = pg;
const db = new Pool({
connectionString: process.env.DATABASE_URL || "postgresql://localhost:5432/my_ai_db"
});
const openai = new OpenAI();
async function getEmbedding(text) {
const res = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text
});
return res.data[0].embedding;
}
// 1. Store a document in PostgreSQL
async function storeDocument(content) {
const embedding = await getEmbedding(content);
// Format the array as a Postgres vector string: "[0.1, -0.4, 0.8...]"
const vectorString = JSON.stringify(embedding);
await db.query(
"INSERT INTO documents (content, embedding) VALUES ($1, $2)",
[content, vectorString]
);
console.log(`Stored: "${content.slice(0, 40)}..."`);
}
// 2. Perform Semantic Search
async function semanticSearch(userQuery, limit = 3) {
const queryEmbedding = await getEmbedding(userQuery);
const vectorString = JSON.stringify(queryEmbedding);
// pgvector operator '<=>' calculates Cosine Distance
// Similarity = 1 - Cosine Distance
const sql = `
SELECT
content,
ROUND((1 - (embedding <=> $1::vector))::numeric, 3) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector ASC
LIMIT $2;
`;
const { rows } = await db.query(sql, [vectorString, limit]);
return rows;
}6. Complete Runnable Example: Semantic FAQ Search
Let's put everything together into a clean JavaScript class:
import pg from "pg";
import OpenAI from "openai";
export class FAQSearchEngine {
constructor(dbConnectionString) {
this.pool = new pg.Pool({ connectionString: dbConnectionString });
this.openai = new OpenAI();
}
async initTable() {
await this.pool.query("CREATE EXTENSION IF NOT EXISTS vector;");
await this.pool.query(`
CREATE TABLE IF NOT EXISTS faqs (
id SERIAL PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
embedding vector(1536)
);
`);
}
async addFAQ(question, answer) {
const res = await this.openai.embeddings.create({
model: "text-embedding-3-small",
input: question
});
const vectorString = JSON.stringify(res.data[0].embedding);
await this.pool.query(
"INSERT INTO faqs (question, answer, embedding) VALUES ($1, $2, $3)",
[question, answer, vectorString]
);
}
async searchFAQ(queryText, similarityThreshold = 0.7) {
const res = await this.openai.embeddings.create({
model: "text-embedding-3-small",
input: queryText
});
const vectorString = JSON.stringify(res.data[0].embedding);
const sql = `
SELECT question, answer, (1 - (embedding <=> $1::vector)) AS similarity
FROM faqs
WHERE (1 - (embedding <=> $1::vector)) >= $2
ORDER BY embedding <=> $1::vector ASC
LIMIT 1;
`;
const { rows } = await this.pool.query(sql, [vectorString, similarityThreshold]);
return rows[0] || null;
}
}
// Example Usage
async function demo() {
const engine = new FAQSearchEngine("postgresql://localhost:5432/my_ai_db");
await engine.initTable();
// Ingest sample FAQs
await engine.addFAQ("How do I cancel my subscription?", "Go to Account Settings -> Billing -> Cancel Plan.");
await engine.addFAQ("What payment methods do you accept?", "We accept Visa, MasterCard, and PayPal.");
// Test with completely different phrasing
const match = await engine.searchFAQ("I want to stop paying every month");
if (match) {
console.log(`Matched FAQ Question: "${match.question}"`);
console.log(`Answer: "${match.answer}"`);
console.log(`Confidence: ${(match.similarity * 100).toFixed(1)}%`);
} else {
console.log("No confident match found.");
}
}7. Understanding the <=> Distance Operator
In pgvector:
<=>calculates Cosine Distance (returns a number between0.0and2.0, where0.0means identical direction).- To convert distance to Cosine Similarity (where
1.0is a 100% match), calculate:1 - (vectorA <=> vectorB).
SELECT
content,
1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector ASC
LIMIT 5;👉 Next Step: Learn how to combine vector search and LLMs to answer questions over your private documents in Building RAG Systems with JavaScript.