Search engines and vector databases
A database answers "give me order 81"; a search engine answers "find products like red running shoes, best first". Keyword search engines have done this for decades with inverted indexes. Vector databases do it by meaning, which is what retrieval for AI assistants needs. Most real systems now combine both. This guide explains how each works, the products, how they differ and which to choose, with a chooser and code you can run.
- Sam What's the difference between a database and a search engine?
- Priya A database answers "give me order eighty-one". A search engine answers "find things like red running shoes, best first". Keyword engines do it with inverted indexes; vector databases do it by meaning. Most real systems now combine both.
- Sam Keyword or vector?
- Priya An inverted index lists, for each word, the documents containing it. It's great for exact terms like product codes and error IDs. Vector search turns each document into an embedding and finds the nearest ones, so "low-cost airfare" finds "cheap flights". Hybrid search runs both and merges them, which is the default for retrieval in AI assistants.
- Sam Walk me through a query.
- Priya The keyword path splits the query into words and scores matching documents, rarer words counting more. The vector path embeds the query and walks a nearest-neighbour graph. Reciprocal rank fusion merges the two lists, and a reranker may reorder the top few. At scale, every shard is asked and the slowest sets the latency.
- Sam What are the options?
- Priya Elasticsearch, built on Lucene, which since August twenty twenty-four is also offered under the AGPL. OpenSearch, the Apache-licensed fork, now under the Linux Foundation. Algolia, Typesense and Meilisearch for instant in-app search. For vectors: pgvector inside PostgreSQL; Pinecone, managed; Milvus, Qdrant and Weaviate, open source; Amazon S3 Vectors, generally available since December twenty twenty-five, with up to two billion vectors per index; and Vertex AI Vector Search and Azure AI Search.
- Sam How do I choose?
- Priya A modest app catalogue: a hosted or lightweight engine. Large catalogues and logs: Elasticsearch or OpenSearch. Retrieval when you already run PostgreSQL: start with pgvector. Hundreds of millions of vectors: a dedicated or managed vector store. And when users type exact codes and natural questions, use hybrid.
- Priya Your turn. A user searches for error code E1234. Which method ranks the exact match first?
- Priya Keyword search. Embeddings capture meaning, not rare exact tokens.
- Sam And the code?
- Priya Five documents and three queries, ranked by keyword, by vector and by hybrid fusion, so you can see where each wins.
- Sam Common mistakes?
- Priya Treating the index as the source of truth. Vectors only. A dedicated vector database for a tiny collection. Forgetting permissions. And mixing embedding models.
1. Two ways to find things
| Keyword search | Vector search | |
|---|---|---|
| Index | An inverted index: each word lists the documents containing it | Each document becomes a vector (an embedding); an approximate nearest-neighbour index (HNSW, IVF, ScaNN) finds close ones |
| Matches | The words you typed (with stemming, synonyms you configure) | Meaning: "low-cost airfare" finds "cheap flights" |
| Good at | Exact terms: product codes, error IDs, names; filters and facets | Paraphrases, questions, other languages, images |
| Weak at | Synonyms you didn't configure | Exact codes and rare words; explaining why something matched |
Hybrid search runs both and merges the results (often by reciprocal rank fusion), then a reranker may reorder the top few. That's the default for retrieval-augmented generation (Module 29).
2. How a search engine answers a query
3. The products
Elasticsearch
Built on Lucene: documents are split into shards, each an inverted index, with replicas for availability. Full-text relevance (BM25), aggregations, and vector fields for kNN and hybrid search. Since August 2024 the free source is also offered under the AGPLv3, an OSI-approved licence, alongside SSPL and Elastic License 2.0.
Best for: product and site search, log analytics, and hybrid search in one engine.
OpenSearch
The fork of Elasticsearch 7.10 started by AWS in 2021, now governed by the OpenSearch Software Foundation under the Linux Foundation. Same model of shards and Lucene indexes, with k-NN vector search and dashboards. Amazon OpenSearch Service runs it managed, including a serverless option.
Best for: Elasticsearch-style search under a permissive licence, or on AWS.
Algolia, Typesense, Meilisearch
Algolia is a hosted search API tuned for instant, typo-tolerant search-as-you-type. Typesense and Meilisearch are simpler open-source engines with the same focus, easy to run for small and medium datasets.
Best for: in-app and e-commerce search without running a cluster.
pgvector
A PostgreSQL extension adding a vector type and HNSW and IVFFlat indexes, so embeddings live next to the rows they describe, with SQL filters and transactions. Available on Cloud SQL, AlloyDB (which adds a ScaNN index), RDS, Aurora and Azure.
Best for: up to tens of millions of vectors when you already run PostgreSQL.
Pinecone
A managed, serverless vector database: namespaces, metadata filters, and hybrid (sparse plus dense) search, with storage separated from compute.
Best for: large vector workloads without running anything.
Milvus, Qdrant, Weaviate
Dedicated vector databases you can run yourself or use as managed clouds (Zilliz for Milvus). They offer several index types, filtering on payloads, quantisation to save memory, and horizontal scaling to billions of vectors.
Best for: very large or self-hosted vector search.
Amazon S3 Vectors
Vector buckets and indexes inside S3, generally available since December 2025, with up to 2 billion vectors per index. Much cheaper storage than a memory-based index, with query latency suited to RAG and agent memory rather than the fastest interactive search.
Best for: large, cost-sensitive vector collections on AWS.
Vertex AI Vector Search, Azure AI Search
Vertex AI Vector Search uses Google's ScaNN algorithm for low-latency nearest-neighbour search at large scale. Azure AI Search combines keyword, vector and semantic ranking in one managed service.
Best for: managed retrieval on those clouds.
4. Differences at a glance
| Option | Keyword | Vector | Scale | Licence or model |
|---|---|---|---|---|
| Elasticsearch | Excellent | Yes (HNSW) | Very large clusters | AGPLv3, SSPL or ELv2; Elastic Cloud |
| OpenSearch | Excellent | Yes (k-NN) | Very large clusters | Apache 2.0; managed on AWS |
| Algolia / Typesense / Meilisearch | Excellent, instant | Supported | Small to large | Hosted API / open source |
| pgvector | PostgreSQL full text | HNSW, IVFFlat | Tens of millions | Open source extension |
| Pinecone | Sparse vectors | Core | Billions | Managed only |
| Milvus / Qdrant / Weaviate | Hybrid supported | Core | Billions | Open source and managed |
| S3 Vectors | No | Core | 2 billion per index | Managed, priced like storage |
| Vertex AI Vector Search | Hybrid supported | ScaNN | Billions | Managed |
5. When to choose which, and why
- Search inside an app or shop, a modest catalogue? A hosted or lightweight engine (Algolia, Typesense, Meilisearch), or PostgreSQL full text if it's small.
- Large catalogues, logs, facets and aggregations? Elasticsearch or OpenSearch.
- Retrieval for an AI assistant, and you already run PostgreSQL? Start with pgvector; the vectors sit next to the data and permissions you already have.
- Hundreds of millions of vectors or more? A dedicated vector database, a managed one, or S3 Vectors when cost matters more than milliseconds.
- Users type exact codes and natural questions? Use hybrid search: keyword and vector, fused, then reranked.
Try it: choose for your requirements
6. Keyword, vector and hybrid search, in code
Five short documents, three queries. Keyword search finds "cheap flight" deals but misses "low-cost airfare" (no shared words); vector search finds it by meaning. For "error E1234", keyword search puts the exact error first, while vector search, which has no idea what E1234 means, prefers the general payment-error page. Hybrid search with reciprocal rank fusion gets both right. The embeddings are three hand-made numbers per word, so the effect is easy to see. The program was run in all three languages; each prints the output below it.
import math
DOCS = {
"d1": "cheap flight deals to paris",
"d2": "low-cost airfare for summer trips",
"d3": "error E1234 when booking a flight",
"d4": "hotel prices in paris",
"d5": "fix payment error on checkout",
}
# Toy 3-number embeddings: [travel, price, error]. Real models output hundreds of numbers learned from text.
EMBED = {
"cheap": [0, 1, 0], "low-cost": [0, 1, 0], "deals": [0, .8, 0], "prices": [0, .9, 0],
"flight": [1, 0, 0], "airfare": [.9, .3, 0], "trips": [.8, 0, 0], "hotel": [.4, .2, 0], "paris": [.3, 0, 0],
"booking": [.5, 0, 0], "error": [0, 0, 1], "fix": [0, 0, .6], "payment": [0, .4, .3], "checkout": [0, .3, .2],
}
def words(text):
return text.lower().split()
def keyword_rank(query):
"""An inverted index scores documents that contain the query's words, rarer words counting more (idf)."""
scores = {}
for doc, text in DOCS.items():
w = words(text)
for term in words(query):
if term in w:
df = sum(1 for t in DOCS.values() if term in words(t))
scores[doc] = scores.get(doc, 0) + math.log(1 + len(DOCS) / df)
return sorted(scores, key=lambda d: (-scores[d], d))
def vector(text):
known = [EMBED[w] for w in words(text) if w in EMBED]
return [sum(v[i] for v in known) / len(known) for i in range(3)] if known else [0, 0, 0]
def cosine(a, b):
na, nb = math.sqrt(sum(x * x for x in a)), math.sqrt(sum(x * x for x in b))
return sum(x * y for x, y in zip(a, b)) / (na * nb) if na and nb else 0
def vector_rank(query, k=3):
"""Vector search ranks every document by how close its meaning is, whether or not words match."""
q = vector(query)
scores = {doc: cosine(q, vector(text)) for doc, text in DOCS.items()}
return sorted(scores, key=lambda d: (-scores[d], d))[:k]
def hybrid(query, k=3):
"""Reciprocal rank fusion: add 1 / (60 + rank) from each list, so documents near the top of either list rise."""
fused = {}
for ranking in (keyword_rank(query), vector_rank(query, len(DOCS))):
for rank, doc in enumerate(ranking, start=1):
fused[doc] = fused.get(doc, 0) + 1 / (60 + rank)
return sorted(fused, key=lambda d: (-fused[d], d))[:k]
for query in ["cheap flight", "low-cost airfare", "error E1234"]:
print(f'"{query}"')
print(" keyword:", ", ".join(keyword_rank(query)) or "(no matches)")
print(" vector: ", ", ".join(vector_rank(query)))
print(" hybrid: ", ", ".join(hybrid(query)))
const DOCS: Record<string, string> = {
d1: "cheap flight deals to paris",
d2: "low-cost airfare for summer trips",
d3: "error E1234 when booking a flight",
d4: "hotel prices in paris",
d5: "fix payment error on checkout",
};
// Toy 3-number embeddings: [travel, price, error]. Real models output hundreds of numbers learned from text.
const EMBED: Record<string, number[]> = {
cheap: [0, 1, 0], "low-cost": [0, 1, 0], deals: [0, 0.8, 0], prices: [0, 0.9, 0],
flight: [1, 0, 0], airfare: [0.9, 0.3, 0], trips: [0.8, 0, 0], hotel: [0.4, 0.2, 0], paris: [0.3, 0, 0],
booking: [0.5, 0, 0], error: [0, 0, 1], fix: [0, 0, 0.6], payment: [0, 0.4, 0.3], checkout: [0, 0.3, 0.2],
};
const words = (text: string) => text.toLowerCase().split(" ");
const byScore = (scores: Map<string, number>) =>
[...scores.keys()].sort((a, b) => scores.get(b)! - scores.get(a)! || (a < b ? -1 : 1));
// An inverted index scores documents that contain the query's words, rarer words counting more (idf).
function keywordRank(query: string): string[] {
const scores = new Map<string, number>();
for (const [doc, text] of Object.entries(DOCS)) {
const w = words(text);
for (const term of words(query)) {
if (!w.includes(term)) continue;
const df = Object.values(DOCS).filter((t) => words(t).includes(term)).length;
scores.set(doc, (scores.get(doc) ?? 0) + Math.log(1 + Object.keys(DOCS).length / df));
}
}
return byScore(scores);
}
function vector(text: string): number[] {
const known = words(text).filter((w) => w in EMBED).map((w) => EMBED[w]);
return known.length ? [0, 1, 2].map((i) => known.reduce((s, v) => s + v[i], 0) / known.length) : [0, 0, 0];
}
function cosine(a: number[], b: number[]): number {
const na = Math.sqrt(a.reduce((s, x) => s + x * x, 0)), nb = Math.sqrt(b.reduce((s, x) => s + x * x, 0));
return na && nb ? a.reduce((s, x, i) => s + x * b[i], 0) / (na * nb) : 0;
}
// Vector search ranks every document by how close its meaning is, whether or not words match.
function vectorRank(query: string, k = 3): string[] {
const q = vector(query);
return byScore(new Map(Object.entries(DOCS).map(([doc, text]) => [doc, cosine(q, vector(text))]))).slice(0, k);
}
// Reciprocal rank fusion: add 1 / (60 + rank) from each list, so documents near the top of either list rise.
function hybrid(query: string, k = 3): string[] {
const fused = new Map<string, number>();
for (const ranking of [keywordRank(query), vectorRank(query, Object.keys(DOCS).length)]) {
ranking.forEach((doc, i) => fused.set(doc, (fused.get(doc) ?? 0) + 1 / (60 + i + 1)));
}
return byScore(fused).slice(0, k);
}
for (const query of ["cheap flight", "low-cost airfare", "error E1234"]) {
console.log(`"${query}"`);
console.log(" keyword:", keywordRank(query).join(", ") || "(no matches)");
console.log(" vector: ", vectorRank(query).join(", "));
console.log(" hybrid: ", hybrid(query).join(", "));
}import java.util.*;
import java.util.stream.*;
public class Main {
static final Map<String, String> DOCS = new LinkedHashMap<>();
static final Map<String, double[]> EMBED = new HashMap<>();
static {
DOCS.put("d1", "cheap flight deals to paris");
DOCS.put("d2", "low-cost airfare for summer trips");
DOCS.put("d3", "error E1234 when booking a flight");
DOCS.put("d4", "hotel prices in paris");
DOCS.put("d5", "fix payment error on checkout");
// Toy 3-number embeddings: [travel, price, error]. Real models output hundreds of numbers learned from text.
Object[][] e = {{"cheap", 0, 1, 0}, {"low-cost", 0, 1, 0}, {"deals", 0, .8, 0}, {"prices", 0, .9, 0},
{"flight", 1, 0, 0}, {"airfare", .9, .3, 0}, {"trips", .8, 0, 0}, {"hotel", .4, .2, 0}, {"paris", .3, 0, 0},
{"booking", .5, 0, 0}, {"error", 0, 0, 1}, {"fix", 0, 0, .6}, {"payment", 0, .4, .3}, {"checkout", 0, .3, .2}};
for (Object[] r : e) EMBED.put((String) r[0], new double[] {((Number) r[1]).doubleValue(), ((Number) r[2]).doubleValue(), ((Number) r[3]).doubleValue()});
}
static List<String> words(String text) { return Arrays.asList(text.toLowerCase().split(" ")); }
static List<String> byScore(Map<String, Double> scores) {
return scores.keySet().stream().sorted(Comparator.comparing((String d) -> -scores.get(d)).thenComparing(d -> d)).collect(Collectors.toList());
}
// An inverted index scores documents that contain the query's words, rarer words counting more (idf).
static List<String> keywordRank(String query) {
Map<String, Double> scores = new HashMap<>();
DOCS.forEach((doc, text) -> {
for (String term : words(query)) {
if (!words(text).contains(term)) continue;
long df = DOCS.values().stream().filter(t -> words(t).contains(term)).count();
scores.merge(doc, Math.log(1 + (double) DOCS.size() / df), Double::sum);
}
});
return byScore(scores);
}
static double[] vector(String text) {
List<double[]> known = words(text).stream().filter(EMBED::containsKey).map(EMBED::get).collect(Collectors.toList());
double[] v = new double[3];
if (known.isEmpty()) return v;
for (int i = 0; i < 3; i++) { double s = 0; for (double[] k : known) s += k[i]; v[i] = s / known.size(); }
return v;
}
static double cosine(double[] a, double[] b) {
double na = 0, nb = 0, dot = 0;
for (double x : a) na += x * x;
for (double x : b) nb += x * x;
for (int i = 0; i < 3; i++) dot += a[i] * b[i];
na = Math.sqrt(na); nb = Math.sqrt(nb);
return na != 0 && nb != 0 ? dot / (na * nb) : 0;
}
// Vector search ranks every document by how close its meaning is, whether or not words match.
static List<String> vectorRank(String query, int k) {
double[] q = vector(query);
Map<String, Double> scores = new HashMap<>();
DOCS.forEach((doc, text) -> scores.put(doc, cosine(q, vector(text))));
return byScore(scores).subList(0, k);
}
// Reciprocal rank fusion: add 1 / (60 + rank) from each list, so documents near the top of either list rise.
static List<String> hybrid(String query, int k) {
Map<String, Double> fused = new HashMap<>();
for (List<String> ranking : List.of(keywordRank(query), vectorRank(query, DOCS.size()))) {
for (int i = 0; i < ranking.size(); i++) fused.merge(ranking.get(i), 1.0 / (60 + i + 1), Double::sum);
}
return byScore(fused).subList(0, k);
}
public static void main(String[] args) {
for (String query : List.of("cheap flight", "low-cost airfare", "error E1234")) {
System.out.println("\"" + query + "\"");
List<String> kw = keywordRank(query);
System.out.println(" keyword: " + (kw.isEmpty() ? "(no matches)" : String.join(", ", kw)));
System.out.println(" vector: " + String.join(", ", vectorRank(query, 3)));
System.out.println(" hybrid: " + String.join(", ", hybrid(query, 3)));
}
}
}"cheap flight"
keyword: d1, d3
vector: d2, d1, d4
hybrid: d1, d3, d2
"low-cost airfare"
keyword: d2
vector: d1, d4, d2
hybrid: d2, d1, d4
"error E1234"
keyword: d3, d5
vector: d5, d3, d1
hybrid: d3, d5, d1
7. Say it in the interview
- "How does search work here?" Writes go to the database, and a change stream (CDC) feeds a search index such as Elasticsearch or OpenSearch, so search is eventually consistent by a second or two. Queries hit the index; the database stays the source of truth.
- "Where do the embeddings live?" In pgvector next to the documents while we're under tens of millions, which keeps filters and permissions in SQL; a dedicated vector database or S3 Vectors beyond that.
- "Why hybrid?" Vectors catch paraphrases, keywords catch exact codes and names; fusing the two lists and reranking the top results beats either alone.
8. Common mistakes
9. Check yourself
A user searches for error code E1234. Which method most reliably ranks the exact match first?
Your app has 2 million documents in PostgreSQL and needs RAG retrieval. A sensible start?
How should a search index be kept up to date with the database?
What does reciprocal rank fusion do?
10. Sources and further reading
Product facts checked on 3 November 2026. Licences and limits change; check before relying on them.
| Source | Used for |
|---|---|
| Elastic announces open source licence for Elasticsearch and Kibana | AGPLv3 option alongside SSPL and ELv2 |
| Amazon S3 Vectors now generally available | General availability and scale |
| S3 Vectors limitations | 2 billion vectors per index |
| pgvector | Index types and usage |
| Module 29: AI systems | Retrieval, embeddings and RAG |