HNSW vs IVFFlat in pgvector: Choosing the Right Vector Index
TL;DR — HNSW vs IVFFlat in pgvector: HNSW is the default for most workloads with 95%+ recall and no rebuild needed. BigDataBoutique: "HNSW is the default for most production vector search as of 2026: high recall, dynamic data, predictable scaling. IVFFlat earns its place when dataset is very large, mostly static, and memory or build time dominates. pgvector exposes both through same SQL surface." DevTo: "HNSW: higher recall, handles incremental inserts. IVFFlat: needs more probes or rebuild if results differ." Medium: "HNSW generally outperforms IVFFlat in search speed, especially for high-recall scenarios. IVFFlat search time grows linearly with probes." Reddit: "HNSW is more set it and forget it. Better recall at same latency once you hit real scale. Downsides: memory and slower index builds." Learn more with pgvector tutorial, vector databases, semantic search, and pgvector vs Pinecone.
BigDataBoutique provides the definitive answer: "HNSW is the default for most production vector search workloads as of 2026: high recall, dynamic data, predictable scaling. IVFFlat earns its place when the dataset is very large, mostly static, and memory or build time dominates the cost model. pgvector exposes both indexes through the same SQL surface, which makes it the easiest stack to test the choice on your own data."
Reddit adds practical context: "HNSW is more set it and forget it and tends to hold better recall at the same latency once you hit real scale. Downsides to switching early are mainly memory and slower index builds."
HNSW vs IVFFlat Architecture
L0: all vectors
L1+: exponentially fewer"] H_Search["Greedy Search
enter coarse layer
descend to fine layer"] H_Insert["Incremental Insert
no rebuild needed
graph adapts"] H_Params["Parameters
m: max connections
ef_construction: build depth
ef_search: query depth"] H_Recall["Recall: 95%+
Latency: fast
Memory: 2-5x IVFFlat"] end subgraph IVFFlat["IVFFlat (Cluster-Based)"] I_Clusters["K-Means Clusters
nlist partitions
Voronoi cells"] I_Search["Probe Search
scan nearest nprobe clusters
brute-force within cluster"] I_Insert["Insert Issues
recall drifts as data changes
may need rebuild"] I_Params["Parameters
lists: number of clusters
probes: clusters to search"] I_Recall["Recall: medium-high
Latency: fast (linear with probes)
Memory: low"] end subgraph Decision["When to Choose"] D_HNSW["HNSW: <10M vectors
active writes
need high recall
enough memory"] D_IVF["IVFFlat: >10M vectors
mostly static
memory constrained
fast build needed"] end H_Layers --> H_Search --> H_Insert H_Params --> H_Recall I_Clusters --> I_Search --> I_Insert I_Params --> I_Recall H_Recall --> D_HNSW I_Recall --> D_IVF
Parameter Comparison
| Parameter | HNSW | IVFFlat | Effect |
|---|---|---|---|
| Build parameter 1 | m (default 16) | lists (recommended sqrt(rows)) | HNSW: max connections per node. IVFFlat: number of clusters. |
| Build parameter 2 | ef_construction (default 64) | — | HNSW: build-time search depth. Higher = better quality, slower build. |
| Query parameter | ef_search (default 40) | probes (default 1) | HNSW: query-time search depth. IVFFlat: clusters to search. Higher = better recall, slower. |
| Set at query time | SET LOCAL hnsw.ef_search = 100 | SET ivfflat.probes = 10 | Use SET LOCAL in transaction for per-query tuning. |
| Needs data to build | No (can create on empty table) | Yes (needs data for k-means) | HNSW can be created before inserting data. |
| Rebuild on data change | No (graph adapts) | Yes (recall drifts) | HNSW absorbs inserts. IVFFlat may need rebuild. |
Performance Comparison
| Metric | HNSW | IVFFlat |
|---|---|---|
| Recall | 95%+ out of the box | Medium-high (depends on probes) |
| Query latency | Fast (graph traversal) | Fast (linear with probes) |
| Memory usage | High (2-5x IVFFlat) | Low |
| Build time | Longer | Faster |
| Insert handling | Absorbs without rebuild | Recall drifts, may need rebuild |
| Empty table support | Yes (create before data) | No (needs data for k-means) |
| Update friendliness | High (incremental) | Medium (drifts over time) |
| Best for | <10M vectors, active writes | >10M vectors, mostly static |
Implementation
from dataclasses import dataclass
from typing import Optional
from enum import Enum
class IndexType(Enum):
HNSW = "hnsw"
IVFFLAT = "ivfflat"
@dataclass
class PgvectorIndexSelector:
"""Select and tune HNSW vs IVFFlat indexes in pgvector."""
def recommend(self, vector_count: int,
active_writes: bool = True,
memory_constrained: bool = False,
need_high_recall: bool = True,
data_loaded: bool = True) -> dict:
"""Recommend HNSW or IVFFlat based on requirements."""
if not data_loaded and not active_writes:
return {
"index": IndexType.HNSW.value,
"reason": "HNSW can be created on empty table. "
"IVFFlat needs data for k-means.",
}
if memory_constrained and vector_count > 10_000_000:
return {
"index": IndexType.IVFFLAT.value,
"reason": "IVFFlat uses 2-5x less memory. "
"Best for large datasets with memory constraints.",
}
if not active_writes and vector_count > 10_000_000:
return {
"index": IndexType.IVFFLAT.value,
"reason": "IVFFlat for large, mostly-static datasets. "
"Builds faster, uses less memory.",
}
return {
"index": IndexType.HNSW.value,
"reason": "HNSW: 95%+ recall, absorbs inserts without "
"rebuild, best latency-recall tradeoff. "
"Default for most workloads as of 2026.",
}
def get_hnsw_sql(self, table: str = "documents",
column: str = "embedding",
op_class: str = "vector_cosine_ops",
m: int = 16,
ef_construction: int = 200) -> str:
"""Generate HNSW index creation SQL."""
return (
f"CREATE INDEX CONCURRENTLY {table}_{column}_hnsw_idx\n"
f"ON {table} USING hnsw ({column} {op_class})\n"
f"WITH (m = {m}, ef_construction = {ef_construction});"
)
def get_ivfflat_sql(self, table: str = "documents",
column: str = "embedding",
op_class: str = "vector_cosine_ops",
lists: int = 100) -> str:
"""Generate IVFFlat index creation SQL."""
return (
f"CREATE INDEX CONCURRENTLY {table}_{column}_ivf_idx\n"
f"ON {table} USING ivfflat ({column} {op_class})\n"
f"WITH (lists = {lists});"
)
def get_hnsw_tuning(self) -> dict:
"""HNSW parameter tuning guide."""
return {
"parameters": {
"m": {
"default": 16,
"range": "4-64",
"effect": "Max connections per node. "
"Higher = more memory, better recall.",
"recommended": {
"fast": 12,
"balanced": 16,
"high_recall": 32,
},
},
"ef_construction": {
"default": 64,
"range": "32-512",
"effect": "Build-time search depth. "
"Higher = better quality, slower build.",
"recommended": {
"fast": 48,
"balanced": 200,
"high_recall": 400,
},
},
"ef_search": {
"default": 40,
"range": "1-1000",
"effect": "Query-time search depth. "
"Higher = better recall, slower query.",
"set_at_query_time": True,
"sql": "SET LOCAL hnsw.ef_search = 100;",
"recommended": {
"fast": 40,
"balanced": 100,
"high_recall": 200,
},
},
},
"notes": (
"HNSW absorbs inserts without quality loss. "
"Graph adapts as nodes arrive. "
"No rebuild step, no maintenance window, no recall drift. "
"Uses 2-5x more memory than IVFFlat."
),
}
def get_ivfflat_tuning(self) -> dict:
"""IVFFlat parameter tuning guide."""
return {
"parameters": {
"lists": {
"default": "sqrt(rows)",
"formula": {
"under_1M": "sqrt(rows)",
"over_1M": "rows / 1000",
},
"effect": "Number of clusters. "
"Higher = finer partitioning, "
"more memory, better recall.",
"examples": {
"100K_vectors": 316,
"1M_vectors": 1000,
"10M_vectors": 10000,
},
},
"probes": {
"default": 1,
"range": "1-100+",
"effect": "Clusters to search at query time. "
"Higher = better recall, slower query. "
"Search time grows linearly with probes.",
"set_at_query_time": True,
"sql": "SET ivfflat.probes = 10;",
"recommended": {
"fast": 1,
"balanced": 10,
"high_recall": 50,
},
},
},
"notes": (
"IVFFlat partitions vectors into k-means clusters. "
"Needs data to build (cannot create on empty table). "
"Recall drifts as data changes — may need rebuild. "
"Uses 2-5x less memory than HNSW. "
"Builds faster than HNSW."
),
}
def get_recall_comparison_sql(self) -> str:
"""SQL to compare HNSW vs IVFFlat recall on your data."""
return (
"-- Compare HNSW vs exact (Flat) search recall\n"
"-- Run on a sample of your data\n"
"\n"
"-- Exact search (ground truth)\n"
"SELECT id FROM documents\n"
"ORDER BY embedding <=> '[0.1,...]'::vector\n"
"LIMIT 100;\n"
"\n"
"-- HNSW search\n"
"SET hnsw.ef_search = 100;\n"
"SELECT id FROM documents\n"
"ORDER BY embedding <=> '[0.1,...]'::vector\n"
"LIMIT 100;\n"
"\n"
"-- IVFFlat search\n"
"SET ivfflat.probes = 10;\n"
"SELECT id FROM documents\n"
"ORDER BY embedding <=> '[0.1,...]'::vector\n"
"LIMIT 100;\n"
"\n"
"-- Compare overlap: if HNSW/IVFFlat results\n"
"-- differ significantly from exact, tune parameters"
)
def get_production_checklist(self) -> list:
"""Production deployment checklist."""
return [
"Use CREATE INDEX CONCURRENTLY to avoid locking",
"SET maintenance_work_mem = '2GB' before building",
"Use SET LOCAL for query-time parameters in transactions",
"Monitor index size: pg_size_pretty(pg_relation_size(...))",
"Monitor recall: compare ANN vs exact on sample queries",
"Monitor latency: EXPLAIN ANALYZE on search queries",
"For HNSW: start with m=16, ef_construction=200, ef_search=100",
"For IVFFlat: start with lists=sqrt(rows), probes=10",
"Operator class must match distance operator",
"Rebuild IVFFlat if data distribution changes significantly",
]
HNSW vs IVFFlat Checklist
- [ ] HNSW is the default for most production vector search workloads as of 2026
- [ ] HNSW: high recall (95%+), dynamic data, predictable scaling
- [ ] HNSW: absorbs inserts without rebuilds — no recall drift
- [ ] HNSW: can be created on empty table — no training step needed
- [ ] HNSW: uses 2-5x more memory than IVFFlat
- [ ] HNSW: longer build time than IVFFlat
- [ ] HNSW: more set it and forget it — better recall at same latency at scale
- [ ] IVFFlat: earns its place when dataset is very large, mostly static, memory-constrained
- [ ] IVFFlat: partitions vectors into k-means Voronoi cells
- [ ] IVFFlat: uses 2-5x less memory than HNSW
- [ ] IVFFlat: builds faster than HNSW
- [ ] IVFFlat: needs data to build (k-means clustering) — cannot create on empty table
- [ ] IVFFlat: recall drifts as data changes — may need rebuild
- [ ] IVFFlat: search time grows linearly with number of probes
- [ ] HNSW generally outperforms IVFFlat in search speed, especially for high-recall scenarios
- [ ] pgvector exposes both indexes through the same SQL surface — test on your own data
- [ ] pgvector 0.8.2 (February 2026): HNSW, IVFFlat, halfvec, sparsevec, binary quantization
- [ ] HNSW parameters: m (max connections, default 16), ef_construction (build depth, default 64), ef_search (query depth, default 40)
- [ ] HNSW tuning: start with m=16, ef_construction=200, ef_search=100
- [ ] HNSW: increase m to 32 for higher recall (more memory)
- [ ] HNSW: increase ef_search at query time for better recall (SET LOCAL hnsw.ef_search = 100)
- [ ] HNSW: increase ef_construction for better build quality (slower build)
- [ ] IVFFlat parameters: lists (number of clusters), probes (clusters to search)
- [ ] IVFFlat lists: sqrt(rows) for <1M vectors, rows/1000 for >1M vectors
- [ ] IVFFlat probes: start with 10, increase for better recall (slower query)
- [ ] IVFFlat: set probes at query time with SET ivfflat.probes = 10
- [ ] If IVFFlat results differ significantly from exact, need more probes or rebuild
- [ ] Use CREATE INDEX CONCURRENTLY to avoid locking production table
- [ ] SET maintenance_work_mem = '2GB' before building indexes
- [ ] Use SET LOCAL for query-time parameters in transactions (applies only to that query)
- [ ] Operator class must match distance operator: vector_cosine_ops for <=>, vector_ip_ops for <#>, vector_l2_ops for <->
- [ ] HNSW SQL: CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 200)
- [ ] IVFFlat SQL: CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)
- [ ] Monitor index size: SELECT pg_size_pretty(pg_relation_size('documents_embedding_hnsw_idx'))
- [ ] Monitor recall: compare ANN results with exact search on sample queries
- [ ] Monitor latency: EXPLAIN ANALYZE on search queries
- [ ] Rebuild IVFFlat if data distribution changes significantly
- [ ] HNSW: no rebuild needed — graph adapts as nodes arrive
- [ ] Choose HNSW for: <10M vectors, active writes, need high recall, enough memory
- [ ] Choose IVFFlat for: >10M vectors, mostly static, memory constrained, fast build needed
- [ ] Downsides to HNSW: memory and slower index builds
- [ ] Downsides to IVFFlat: recall drift, needs data to build, may need rebuild
- [ ] Test both on your own data — pgvector makes it easy with same SQL surface
- [ ] Read pgvector tutorial for setup
- [ ] Read vector databases for ANN fundamentals
- [ ] Read semantic search for implementation
- [ ] Read pgvector vs Pinecone for comparison
- [ ] Read cosine similarity for distance metrics
- [ ] Test: HNSW achieves 95%+ recall with default parameters
- [ ] Test: IVFFlat recall improves with more probes
- [ ] Test: HNSW absorbs inserts without recall degradation
- [ ] Test: IVFFlat recall drifts after significant data changes
- [ ] Test: recommendation logic selects correct index per use case
- [ ] Document index choice, parameters, tuning strategy, and monitoring plan
FAQ
Should you use HNSW or IVFFlat in pgvector?
Use HNSW for most production workloads — it has 95%+ recall, absorbs inserts without rebuilds, and is the default as of 2026. Use IVFFlat only for large, mostly-static datasets where memory or build time dominates. BigDataBoutique: "HNSW is the default for most production vector search workloads as of 2026: high recall, dynamic data, predictable scaling. IVFFlat earns its place when the dataset is very large, mostly static, and memory or build time dominates the cost model. pgvector exposes both indexes through the same SQL surface, which makes it the easiest stack to test the choice on your own data." DevTo: "HNSW: higher recall, handles incremental inserts. CREATE INDEX CONCURRENTLY idx_docs_hnsw ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 200)." Reddit: "HNSW is more set it and forget it and tends to hold better recall at the same latency once you hit real scale. Downsides to switching early are mainly memory and slower index builds."
What are the HNSW parameters in pgvector and how do you tune them?
HNSW has three key parameters: m (max connections per node), ef_construction (build-time search depth), and ef_search (query-time search depth). BigDataBoutique: "HNSW is a graph-based vector index that reaches 95%+ recall out of the box and absorbs inserts without rebuilds, but uses 2-5x more memory than IVFFlat." Medium: "HNSW: Adjust ef_search at query time. Higher ef_search increases recall but decreases speed." DevTo: "CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 200)." Parameters: (1) m: max connections per node (default 16, higher = more memory, better recall). (2) ef_construction: build-time search depth (default 64, higher = better quality, slower build). (3) ef_search: query-time search depth (default 40, set with SET LOCAL hnsw.ef_search = 100). Tuning: start with m=16, ef_construction=200, ef_search=100. Increase m to 32 for higher recall. Increase ef_search for better recall at query time.
What are the IVFFlat parameters in pgvector and how do you tune them?
IVFFlat has two key parameters: lists (number of clusters) and probes (clusters to search at query time). BigDataBoutique: "IVFFlat partitions vectors into k-means Voronoi cells, builds faster, and uses less memory, but its recall drifts as data changes. Choose IVFFlat for very large, mostly static datasets where memory or build time dominates." Medium: "IVFFlat: Adjust nprobe at query time. More probes increase recall but decrease speed linearly. IVFFlat search time grows linearly with the number of probes." DevTo: "If results differ significantly, your IVFFlat index needs more probes or a full rebuild with better list parameters." Parameters: (1) lists: number of clusters (recommended sqrt(rows) for <1M, rows/1000 for >1M). (2) probes: clusters to search (default 1, set with SET ivfflat.probes = 10). Tuning: start with lists=sqrt(rows), probes=10. Increase probes for better recall. Rebuild index if data distribution changes significantly.
When does IVFFlat outperform HNSW in pgvector?
IVFFlat outperforms HNSW when the dataset is very large, mostly static, and memory or build time is the primary constraint. BigDataBoutique: "IVFFlat earns its place when the dataset is very large, mostly static, and memory or build time dominates the cost model. HNSW generally wins on query quality and latency. IVF wins on memory and disk-friendliness." Medium: "HNSW generally outperforms IVFFlat in search speed, especially for high-recall scenarios." Reddit: "Downsides to switching early to HNSW are mainly memory and slower index builds. With 1.5GB pods you need to watch index size." IVFFlat wins when: (1) Dataset is very large (>10M vectors) and memory is constrained. (2) Data is mostly static (few inserts/updates). (3) Build time matters (IVFFlat builds faster). (4) Memory footprint is critical (IVFFlat uses 2-5x less). (5) You can tolerate periodic rebuilds for recall maintenance.
How do you create HNSW and IVFFlat indexes in pgvector?
Create HNSW with CREATE INDEX USING hnsw and IVFFlat with CREATE INDEX USING ivfflat, specifying operator class and parameters. BigDataBoutique: "pgvector exposes both indexes through the same SQL surface, which makes it the easiest stack to test the choice on your own data. pgvector 0.8.2 (February 2026) supports HNSW, IVFFlat, halfvec, sparsevec, and binary quantization." DevTo: "CREATE INDEX CONCURRENTLY idx_docs_hnsw ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 200)." HNSW: CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 200). IVFFlat: CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100). Use CONCURRENTLY for production to avoid locking. Operator class must match distance operator: vector_cosine_ops for <=>, vector_ip_ops for <#>, vector_l2_ops for <->.
Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →