Aurora pgvector HNSW Tuning: Keep Code Symbol Search Under SLOs

Aurora pgvector HNSW Tuning: Keep Code Symbol Search Under SLOs
8 views
Code-symbol nearest-neighbor search is useless if p95 spikes past your IDE chat budget when the index crosses a few million embeddings. Aurora PostgreSQL with pgvector HNSW is fast when m, ef_construction, and especially ef_search match your recall/latency envelope—not when you copy blog defaults into a 50M-row symbol table.
⚡ TL;DR: Build HNSW with m=16–32 and high ef_construction; set session ef_search from a latency/recall sweep on golden symbol queries; pin Aurora instance class + shared_buffers so the graph stays warm; monitor EXPLAIN (ANALYZE, BUFFERS) and CloudWatch ReadIOPS; never raise ef_search blindly to “fix” bad chunking. Pair with RAG Evaluation on AWS and Embedding Drift Alerts.

Aurora pgvector HNSW Tuning: production guidance

sql
-- Explicit ops class + HNSW params for 1536-d Titan / Cohere embeddings
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE code_symbols (
 id bigserial PRIMARY KEY,
 repo text NOT NULL,
 path text NOT NULL,
 symbol text NOT NULL,
 language text NOT NULL,
 embedding vector(1536) NOT NULL,
 updated_at timestamptz NOT NULL DEFAULT now()
);

-- Build offline / off-peak; ef_construction hurts build time, not query latency
CREATE INDEX CONCURRENTLY code_symbols_hnsw
 ON code_symbols
 USING hnsw (embedding vector_cosine_ops)
 WITH (m = 24, ef_construction = 200);

CREATE INDEX code_symbols_repo_lang ON code_symbols (repo, language);
Avoid ivfflat with a tiny lists count on a growing monorepo — recall collapses as partitions fill. Prefer HNSW for interactive symbol search unless you have a hard memory ceiling and a stable corpus.

Session ef_search from a real sweep

ef_search is the knob that trades latency for recall at query time. Sweep it against a frozen golden set of “find this symbol / API” questions and pick the lowest value that clears your hit@10 SLO.
python
# tune_ef_search.py — run against a read replica
import psycopg2, time, json

GOLDEN = json.load(open("golden_symbol_queries.json"))
CANDIDATES = [40, 64, 80, 100, 128, 200]

def recall_at_k(conn, emb, expect_ids, ef, k=10):
 with conn.cursor() as cur:
 cur.execute("SET hnsw.ef_search = %s", (ef,))
 cur.execute(
 """
 SELECT id FROM code_symbols
 ORDER BY embedding <=> %s::vector
 LIMIT %s
 """,
 (emb, k),
 )
 got = {r[0] for r in cur.fetchall()}
 return 1.0 if any(i in got for i in expect_ids) else 0.0

def sweep(dsn: str):
 conn = psycopg2.connect(dsn)
 rows = []
 for ef in CANDIDATES:
 hits, lat = [], []
 for q in GOLDEN:
 t0 = time.perf_counter()
 hits.append(recall_at_k(conn, q["embedding"], q["expect_ids"], ef))
 lat.append((time.perf_counter() - t0) * 1000)
 rows.append({
 "ef_search": ef,
 "hit@10": sum(hits) / len(hits),
 "p95_ms": sorted(lat)[int(0.95 * len(lat))],
 })
 return rows
ef_search Typical hit@10 p95 (warm) When to use
40–64 0.88–0.93 <30 ms Autocomplete / IDE hover
80–100 0.94–0.97 30–60 ms Chat RAG default
128–200 0.97–0.99 60–120 ms Offline eval / rare deep searches

Keep the graph warm under Aurora

HNSW is memory-sensitive. Undersized instances thrash through the graph and destroy p95 even with perfect ef_search.
sql
-- Probe buffer health on a representative query
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT id, path, symbol
FROM code_symbols
WHERE repo = 'payments'
ORDER BY embedding <=> $1
LIMIT 10;
  • Prefer Aurora memory-optimized classes so the HNSW index + hot pages fit in RAM.
  • Use a reader endpoint for search; keep writers for ingest/rebuild only.
  • Filter with repo / language after ANN only if selective — better: maintain per-repo indexes or partition when one tenant dominates.

Operational guardrails

bash
# CloudWatch alarms that catch silent recall death
# - FreeableMemory declining while ReadIOPS climbs → index not warm
# - RDS CPUCreditBalance (T-class) → never put ANN on burstable for prod chat
# - Custom metric: p95_nn_ms from the app, not just DB CPU
Rebuild with CREATE INDEX CONCURRENTLY after embedding-model swaps; do not REINDEX in place during peak. Track embedding model ID as a column so mixed-generation vectors never share one HNSW graph.

Closing checklist

Dos – Sweep ef_search on golden symbol queries; pin the winner in app session setup – Size Aurora so HNSW stays resident; watch EXPLAIN (BUFFERS) – Combine ANN with metadata filters (repo, language) intentionally – Rebuild indexes after embedding model changes – Measure hit@k and p95 together before shipping Donts – Do not copy ef_search=100 from a tutorial without a sweep – Do not run interactive ANN on T-class burstable instances – Do not mix embedding generations in one index – Do not “fix” bad chunking by cranking ef_search to 400 – Do not rebuild HNSW on the writer during peak traffic

Related reading

Last updated on September 11, 2026 Related: Hybrid Search for Code: BM25, Vectors, and Symbol Indexes Together; OpenSearch vs pgvector for Codebase RAG on AWS; AWS Lambda SnapStart: Sub-100ms Cold Starts for Java Without Changing Your Code.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.