What is semantic search and how is it different from keyword search?
Keyword search matches exact words in a query; semantic search matches meaning, so it finds relevant results even when no words overlap. Embeddings are what make semantic search possible.
AIEmbeddingsSearch
Overview
Traditional search (Elasticsearch, SQL LIKE) matches documents that contain the exact words you searched for. Semantic search converts both queries and documents to embedding vectors and finds the ones closest in meaning - regardless of the specific words used. This is why "how do I reset my login?" can return a document titled "password recovery guide" even though no words overlap.
Key Points
- •Keyword search: fast, exact, works well when users know the right terminology.
- •Semantic search: finds conceptual matches even with zero word overlap - critical for natural language queries.
- •Hybrid search combines both: keyword recall + semantic reranking gives the best results in practice.
- •BM25 is the standard keyword algorithm used by Elasticsearch, Solr, and most search engines.
- •Reranking (cross-encoders like Cohere Rerank) adds a third stage: take the top-50 semantic matches and score them precisely.
- •Most production search pipelines today use: BM25 (keyword) + embedding ANN (semantic) + reranker = excellent results.
Examples
Keyword vs semantic search comparison
typescriptconst documents = [
{ id: 1, text: 'How to reset your password' },
{ id: 2, text: 'Billing and payment methods' },
{ id: 3, text: 'Account recovery guide' },
{ id: 4, text: 'Two-factor authentication setup' },
];
const query = 'I forgot my login credentials';
// Keyword search: looks for exact word matches
// "forgot", "login", "credentials" - none appear in doc titles
// Result: no matches found
// Semantic search: finds documents by meaning
// "forgot login credentials" is conceptually close to "reset password" and "account recovery"
// Result: doc 1 (similarity 0.91), doc 3 (similarity 0.87)Hybrid search implementation
typescript// Hybrid search: combine BM25 keyword scores + embedding cosine scores
async function hybridSearch(query: string, topK = 10) {
// Run both searches in parallel
const [keywordResults, semanticResults] = await Promise.all([
elasticsearchClient.search({
index: 'documents',
body: {
query: { match: { text: query } }, // BM25 keyword search
size: topK * 2,
},
}),
vectorDB.query({
vector: await getEmbedding(query), // semantic vector search
topK: topK * 2,
includeMetadata: true,
}),
]);
// Reciprocal Rank Fusion (RRF): combine the two result lists
const scores = new Map<string, number>();
const K = 60; // RRF constant
keywordResults.hits.hits.forEach((hit, rank) => {
const id = hit._id;
scores.set(id, (scores.get(id) ?? 0) + 1 / (K + rank + 1));
});
semanticResults.matches.forEach((match, rank) => {
const id = match.id;
scores.set(id, (scores.get(id) ?? 0) + 1 / (K + rank + 1));
});
// Sort by combined RRF score and return top K
return [...scores.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, topK)
.map(([id, score]) => ({ id, score }));
}Reranking with Cohere for final precision
typescriptimport { CohereClient } from 'cohere-ai';
const cohere = new CohereClient({ token: process.env.COHERE_API_KEY });
async function searchWithRerank(query: string) {
// Stage 1: fast retrieval (get 50 candidates cheaply)
const candidates = await hybridSearch(query, 50);
const texts = candidates.map(c => c.text);
// Stage 2: precise reranking (cross-encoder scores each candidate against the query)
const reranked = await cohere.rerank({
model: 'rerank-english-v3.0',
query,
documents: texts,
topN: 5, // keep only the best 5
});
return reranked.results.map(r => ({
text: texts[r.index],
score: r.relevanceScore,
}));
}Pro Tip
Do not replace keyword search with semantic search - combine them. Keyword search has perfect recall for exact product names, error codes, and IDs. Semantic search handles natural language questions. Hybrid search with RRF fusion beats either alone by 15-25% on standard benchmarks.