LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnApplied LLM EngineeringDimensionality Reduction for Embeddings
📐MediumEmbeddings & Vector Search

Dimensionality Reduction for Embeddings

Shrink and inspect embedding indexes without guessing: measure recall while testing PCA, projections, native shortening, and quantization.

26 min read
Learning path
Step 61 of 196 in the full curriculum
Instruction Tuning & Chat TemplatesCoT, ToT & Self-Consistency Prompting

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The last chapter taught an access-policy assistant to answer with the same chat template at train and serve time. Now consider a failure before generation: the assistant receives the right question but retrieves the wrong policy clause. "My service-account key is 18 days old. Can I keep using it?" should land near "Keys older than 14 days require a rotation ticket," not near an auto-merge rule.

Each clause is stored as a long embedding. An engineer proposes shortening every vector to save memory. What evidence would make that safe? A cleaner-looking plot isn't enough; the rotation clause still has to survive the top-KKK retrieval that supplies the answer.

Keep two jobs separate:

  1. Can we store and search shorter vectors while still returning the same useful policy chunks?
  2. Can we draw a two-dimensional map that helps an engineer inspect confusing clusters?

Both jobs use dimensionality reduction, but they don't share a success criterion. A production vector is approved by retrieval quality and cost. A plot is approved by what it lets you investigate, with important conclusions checked back in the original embedding space.

The article keeps one policy index in view while changing one representation at a time. First establish cost and original neighbors. Then project, shorten, or quantize and ask which neighbors survived.

A shared 1536-dimensional policy embedding space splits into two paths: a 256-dimensional serving vector checked by Recall@K and a separate 2D map used to inspect clusters and outliers.
Reduction has two different jobs: serving candidates must preserve relevant neighbors under a cost budget, while 2D maps help investigate records without becoming the production index.
Diagram showing Long embedding, Fewer dims or bits, Recall@K + RAM, and Serve.
Long embedding, Fewer dims or bits, Recall@K + RAM, and Serve.

Start with the budget, not an algorithm

Before choosing a reducer, put the storage pressure on paper. An embedding is an array of numbers, and float32 storage costs four bytes per dimension. Storing ten million 1,536-dimensional policy or document vectors therefore costs:

10,000,000×1,536×4=61,440,000,000 bytes10{,}000{,}000 \times 1{,}536 \times 4 = 61{,}440{,}000{,}000 \text{ bytes}10,000,000×1,536×4=61,440,000,000 bytes

That's 61.44 GB using decimal gigabytes, before the index graph, metadata, replicas, or backups. Cutting the vector to 256 dimensions makes the raw array six times smaller. The arithmetic proves a cost change, not a safe search change.

Run the arithmetic first. It turns "embeddings feel large" into a concrete engineering constraint.

estimate-vector-storage.py
1def decimal_gb(byte_count: int) -> float: 2 return byte_count / 1_000_000_000 3 4documents = 10_000_000 5bytes_per_float32 = 4 6full_dim = 1_536 7candidate_dim = 256 8 9full_bytes = documents * full_dim * bytes_per_float32 10candidate_bytes = documents * candidate_dim * bytes_per_float32 11 12print(f"Full float32 array: {decimal_gb(full_bytes):.2f} GB") 13print(f"256-d float32 array: {decimal_gb(candidate_bytes):.2f} GB") 14print(f"Raw-vector reduction: {full_bytes / candidate_bytes:.1f}x")
Output
1Full float32 array: 61.44 GB 2256-d float32 array: 10.24 GB 3Raw-vector reduction: 6.0x

The first applied habit is simple: separate a cost metric from a quality metric.

QuestionUseful metric
Will vectors fit in memory?Bytes per vector, total RAM, index overhead
Will useful evidence still be retrieved?Recall@K, nDCG@K, answer success on grounded evaluations
Will an engineer understand failures?A labeled plot plus checks in the original space

Why can't a six-times smaller vector index be approved from storage savings alone?

Answer

Storage reports the benefit, not the semantic loss. A shorter index can omit the policy chunk needed to answer a request correctly. Approval requires retrieval or downstream answer measurements on representative queries.

Build a retrieval baseline

Storage has a number now, but quality still needs a reference. Before compressing anything, define what "the same useful neighbors" means. The small fixture below represents six policy clauses. Its coordinates are hand-written so you can see the experiment without needing an API key: the first two coordinates concern key rotation, the next two concern merge gates, and the last two concern access escalations.

Cosine similarity compares directions rather than raw lengths. We L2-normalize each vector, score one 18-day-key query, and save the top results as the baseline that reduction must try to preserve.

Before running the cell, predict its first two clauses. The query points toward the first two coordinates, so the two rotation clauses should lead; the later merge and access clauses are controls for a false neighbor.

baseline-policy-retrieval.py
1import math 2 3documents = [ 4 "Keys older than 14 days require a rotation ticket before continued use.", 5 "Rotate a stale service-account key and open a ticket before reuse.", 6 "Auto-merge only when tests, security scan, and reviewer approval all pass.", 7 "A failed security scan blocks merge even if tests are green.", 8 "Access escalations need an owner and an expiry; never grant standing admin.", 9 "Standing production admin is forbidden; use a time-bounded escalation.", 10] 11 12vectors = [ 13 [1.00, 0.92, 0.04, 0.00, 0.02, 0.00], 14 [0.92, 0.80, 0.04, 0.01, 0.02, 0.00], 15 [0.02, 0.00, 1.00, 0.88, 0.04, 0.00], 16 [0.02, 0.02, 0.91, 0.98, 0.00, 0.00], 17 [0.00, 0.00, 0.04, 0.02, 1.00, 0.89], 18 [0.02, 0.00, 0.00, 0.02, 0.91, 1.00], 19] 20query = [0.98, 1.00, 0.03, 0.00, 0.01, 0.00] 21 22def dot(left: list[float], right: list[float]) -> float: 23 return sum(a * b for a, b in zip(left, right)) 24 25def normalize(vector: list[float]) -> list[float]: 26 scale = math.sqrt(dot(vector, vector)) 27 return [value / scale for value in vector] 28 29query_hat = normalize(query) 30scores = [dot(normalize(vector), query_hat) for vector in vectors] 31ranking = sorted(range(len(scores)), key=lambda index: -scores[index]) 32 33for rank, index in enumerate(ranking[:3], start=1): 34 print(f"{rank}. {documents[index]} score={scores[index]:.3f}")
Output
11. Keys older than 14 days require a rotation ticket before continued use. score=0.999 22. Rotate a stale service-account key and open a ticket before reuse. score=0.997 33. A failed security scan blocks merge even if tests are green. score=0.036

In a real evaluation set, each query has judged relevant documents. For a query with two relevant rotation clauses, recall@2 = 1.0 means both appear in the top two returned results. That's the comparison target for every compressed representation here.

PCA: learn a shorter linear coordinate system

The first post-hoc candidate changes coordinates without retraining the encoder. Principal component analysis (PCA) fits a linear transformation to a corpus: center the embedding matrix, find directions with the most variation, and keep the first k directions. If XXX is the centered document matrix and VkV_kVk​ holds the chosen directions, the reduced vectors are:

Z=XVkZ = X V_kZ=XVk​

The rows are still documents, now with fewer columns. In implementations, singular value decomposition (SVD) is a practical way to find principal directions without explicitly constructing a covariance matrix.[1]Reference 1Numerical Linear Algebra.https://people.maths.ox.ac.uk/trefethen/books.html The labs use power iteration so you can run them with the standard library; the geometry is the same idea.

A centered embedding cloud shows the first principal axis capturing the largest spread, while a single fitted mean and basis transform both document and query matrices into comparable reduced coordinates.
PCA learns variance-aligned axes from representative documents. Persist the mean and basis, then project stored documents and incoming queries through that same coordinate system.

Follow the right-hand path in the figure before reading the code: one fitted mean and basis must transform both sides of the search. Fitting PCA separately on document vectors and live queries creates different axes, so their dot products no longer compare like with like. Persist the transform fitted on representative documents and reuse it for indexed documents and future queries.

The next cell adds small variations around the three policy themes, fits a two-dimensional PCA model, and retrieves with a query transformed by that same model. Predict the final label first: the query still describes rotation, even though its coordinates will change.

fit-one-pca-transform.py
1import math 2import random 3 4centers = [ 5 [1.0, 0.9, 0.0, 0.0, 0.0, 0.0], # rotation 6 [0.0, 0.0, 1.0, 0.9, 0.0, 0.0], # merge 7 [0.0, 0.0, 0.0, 0.0, 1.0, 0.9], # access 8] 9labels = ["rotation"] * 20 + ["merge"] * 20 + ["access"] * 20 10rng = random.Random(4) 11documents = [ 12 [center[j] + rng.gauss(0, 0.05) for j in range(6)] 13 for center in centers 14 for _ in range(20) 15] 16query = [[0.96, 0.91, 0.02, 0.01, 0.00, 0.00]] 17 18def dot(left: list[float], right: list[float]) -> float: 19 return sum(a * b for a, b in zip(left, right)) 20 21def normalize(vector: list[float]) -> list[float]: 22 scale = math.sqrt(dot(vector, vector)) 23 return [value / scale for value in vector] 24 25def mean_col(rows: list[list[float]]) -> list[float]: 26 n_rows, n_cols = len(rows), len(rows[0]) 27 return [sum(row[j] for row in rows) / n_rows for j in range(n_cols)] 28 29def sub(left: list[float], right: list[float]) -> list[float]: 30 return [a - b for a, b in zip(left, right)] 31 32def fit_pca(rows: list[list[float]], k: int, seed: int = 0) -> tuple[list[float], list[list[float]]]: 33 mu = mean_col(rows) 34 centered = [sub(row, mu) for row in rows] 35 n_cols = len(rows[0]) 36 local = random.Random(seed) 37 components: list[list[float]] = [] 38 for _ in range(k): 39 vector = normalize([local.gauss(0, 1) for _ in range(n_cols)]) 40 for _ in range(80): 41 projected = [dot(row, vector) for row in centered] 42 vector = [ 43 sum(centered[i][j] * projected[i] for i in range(len(centered))) 44 for j in range(n_cols) 45 ] 46 for previous in components: 47 coeff = dot(vector, previous) 48 vector = [value - coeff * axis for value, axis in zip(vector, previous)] 49 vector = normalize(vector) 50 components.append(vector) 51 return mu, components 52 53def transform(rows: list[list[float]], mu: list[float], components: list[list[float]]) -> list[list[float]]: 54 return [[dot(sub(row, mu), axis) for axis in components] for row in rows] 55 56def explained_ratio(rows: list[list[float]], mu: list[float], components: list[list[float]]) -> float: 57 centered = [sub(row, mu) for row in rows] 58 total = sum(dot(row, row) for row in centered) 59 kept = 0.0 60 for axis in components: 61 scores = [dot(row, axis) for row in centered] 62 kept += sum(score * score for score in scores) 63 return kept / total 64 65mu, components = fit_pca(documents, k=2) 66reduced_documents = [normalize(row) for row in transform(documents, mu, components)] 67reduced_query = normalize(transform(query, mu, components)[0]) 68best_index = max(range(len(reduced_documents)), key=lambda i: dot(reduced_documents[i], reduced_query)) 69 70print(f"Shape: ({len(documents)}, {len(documents[0])}) -> ({len(reduced_documents)}, {len(reduced_documents[0])})") 71print(f"Explained variance kept: {explained_ratio(documents, mu, components):.3f}") 72print(f"Closest policy theme: {labels[best_index]}")
Output
1Shape: (60, 6) -> (60, 2) 2Explained variance kept: 0.992 3Closest policy theme: rotation

PCA optimizes variance, not relevance. A direction that rarely changes in the corpus can still distinguish "auto-merge allowed" from "security scan failed." Explained variance helps diagnose what the projection retained; retrieval recall decides whether the index is safe to ship.

Sweep dimensions against neighbor recall

One successful query doesn't establish a dimension. On a small data set, you can provide the relevance judgments yourself. This lab creates twenty policy intents in a noisy 24-dimensional space, gives each query three acceptable chunks, and tries PCA dimensions 2, 4, 8, and 12. Predict the tradeoff: the first cuts should save more coordinates, but may lose neighbors.

pca-recall-sweep.py
1import math 2import random 3 4rng = random.Random(21) 5dimensions = 24 6intent_count = 20 7acceptable_chunks = 3 8 9def dot(left: list[float], right: list[float]) -> float: 10 return sum(a * b for a, b in zip(left, right)) 11 12def normalize(vector: list[float]) -> list[float]: 13 scale = math.sqrt(dot(vector, vector)) 14 return [value / scale for value in vector] 15 16def mean_col(rows: list[list[float]]) -> list[float]: 17 n_rows, n_cols = len(rows), len(rows[0]) 18 return [sum(row[j] for row in rows) / n_rows for j in range(n_cols)] 19 20def sub(left: list[float], right: list[float]) -> list[float]: 21 return [a - b for a, b in zip(left, right)] 22 23def fit_pca(rows: list[list[float]], k: int, seed: int = 0) -> tuple[list[float], list[list[float]]]: 24 mu = mean_col(rows) 25 centered = [sub(row, mu) for row in rows] 26 n_cols = len(rows[0]) 27 local = random.Random(seed) 28 components: list[list[float]] = [] 29 for _ in range(k): 30 vector = normalize([local.gauss(0, 1) for _ in range(n_cols)]) 31 for _ in range(60): 32 projected = [dot(row, vector) for row in centered] 33 vector = [ 34 sum(centered[i][j] * projected[i] for i in range(len(centered))) 35 for j in range(n_cols) 36 ] 37 for previous in components: 38 coeff = dot(vector, previous) 39 vector = [value - coeff * axis for value, axis in zip(vector, previous)] 40 vector = normalize(vector) 41 components.append(vector) 42 return mu, components 43 44def transform(rows: list[list[float]], mu: list[float], components: list[list[float]]) -> list[list[float]]: 45 return [[dot(sub(row, mu), axis) for axis in components] for row in rows] 46 47def explained_ratio(rows: list[list[float]], mu: list[float], components: list[list[float]]) -> float: 48 centered = [sub(row, mu) for row in rows] 49 total = sum(dot(row, row) for row in centered) 50 kept = 0.0 51 for axis in components: 52 scores = [dot(row, axis) for row in centered] 53 kept += sum(score * score for score in scores) 54 return kept / total 55 56def top_k(query_rows: list[list[float]], doc_rows: list[list[float]], k: int) -> list[list[int]]: 57 doc_hat = [normalize(row) for row in doc_rows] 58 ranking: list[list[int]] = [] 59 for query in query_rows: 60 query_hat = normalize(query) 61 scores = [dot(doc, query_hat) for doc in doc_hat] 62 order = sorted(range(len(scores)), key=lambda index: -scores[index]) 63 ranking.append(order[:k]) 64 return ranking 65 66intent_vectors = [[rng.gauss(0, 1) for _ in range(dimensions)] for _ in range(intent_count)] 67center = mean_col(intent_vectors) 68intent_vectors = [normalize(sub(vector, center)) for vector in intent_vectors] 69 70documents: list[list[float]] = [] 71queries: list[list[float]] = [] 72relevant: list[set[int]] = [] 73for intent in intent_vectors: 74 first_chunk = len(documents) 75 for _ in range(acceptable_chunks): 76 documents.append([intent[j] + rng.gauss(0, 0.11) for j in range(dimensions)]) 77 queries.append([intent[j] + rng.gauss(0, 0.03) for j in range(dimensions)]) 78 relevant.append(set(range(first_chunk, first_chunk + acceptable_chunks))) 79 80for target_dim in [2, 4, 8, 12]: 81 mu, components = fit_pca(documents, k=target_dim) 82 short_docs = transform(documents, mu, components) 83 short_queries = transform(queries, mu, components) 84 returned = top_k(short_queries, short_docs, k=3) 85 recall = sum( 86 len(expected & set(found)) / acceptable_chunks 87 for expected, found in zip(relevant, returned) 88 ) / len(relevant) 89 print( 90 f"{target_dim:>2} dims: recall@3={recall:.3f}, " 91 f"variance={explained_ratio(documents, mu, components):.3f}" 92 )
Output
12 dims: recall@3=0.383, variance=0.294 2 4 dims: recall@3=0.867, variance=0.495 3 8 dims: recall@3=1.000, variance=0.727 412 dims: recall@3=1.000, variance=0.871

These numbers belong to this generated fixture, not to all embedding models. The two curves answer different questions: variance describes the projection, while relevant-chunk recall describes the product. Keep the experiment pattern, then replace its documents, queries, and judgments with data from your own retrieval workload.

A PCA model keeps 95 percent of explained variance but loses a relevant rotation-ticket chunk from top-5 retrieval. Should it ship?

Answer

No. PCA preserved high-variance directions, but the product needs correct retrieval. Reduce less aggressively, try another method, or revise the index only after the retrieval metric meets the required threshold.

A two-dimensional map is an investigation tool

The baseline and PCA experiment protect serving quality with judged neighbors. Debugging needs a different view. A two-dimensional projection can help an engineer ask:

  • Are 18-day-key queries scattered into the merge-gate cluster?
  • Are clauses from a new policy version isolated from older versions?
  • Which points should an engineer inspect in the original data?

It doesn't become a search index just because the plot looks separated. Compressing a high-dimensional semantic vector to two coordinates forces distortion, so use the map to choose cases for inspection and return to the original vectors for a decision.

Compare a linear map and a prettier picture

PCA already gave us a deterministic linear view. T-distributed stochastic neighbor embedding (t-SNE) and Uniform Manifold Approximation and Projection (UMAP) focus more strongly on neighborhood relationships, using nonlinear objectives. The visual difference matters because a layout can make clusters appear convincingly separated even when a production retrieval test would disagree.

The same three policy groups form an overlapping diagonal under PCA, separated local islands under t-SNE, and a UMAP neighbor-graph view, with one anchor point keeping its nearest neighbors in each map.
Each method tells a different 2D story. Treat local neighborhoods as investigation leads, not proof that cluster gaps or production retrieval are faithful.

The next lab uses the same six policy vectors as the baseline. Before running it, predict what a pretty island map could hide: a merge clause might look close to a rotation clause even when their original vectors are far apart. The cell reports each clause's original 1-nearest neighbor, then compares PCA with a cleaned-up island picture that an engineer might be tempted to trust.

pretty-maps-can-lie.py
1import math 2 3names = ["rot-a", "rot-b", "merge-a", "merge-b", "access-a", "access-b"] 4vectors = [ 5 [1.00, 0.92, 0.04, 0.00, 0.02, 0.00], 6 [0.92, 0.80, 0.04, 0.01, 0.02, 0.00], 7 [0.02, 0.00, 1.00, 0.88, 0.04, 0.00], 8 [0.02, 0.02, 0.91, 0.98, 0.00, 0.00], 9 [0.00, 0.00, 0.04, 0.02, 1.00, 0.89], 10 [0.02, 0.00, 0.00, 0.02, 0.91, 1.00], 11] 12pretty_map = [ 13 [0.00, 0.00], 14 [0.20, 0.10], 15 [0.30, 0.05], # merge-a pulled into the rotation island 16 [2.00, 2.00], 17 [0.00, 2.00], 18 [0.20, 2.10], 19] 20 21def dot(left: list[float], right: list[float]) -> float: 22 return sum(a * b for a, b in zip(left, right)) 23 24def mean_col(rows: list[list[float]]) -> list[float]: 25 n_rows, n_cols = len(rows), len(rows[0]) 26 return [sum(row[j] for row in rows) / n_rows for j in range(n_cols)] 27 28def sub(left: list[float], right: list[float]) -> list[float]: 29 return [a - b for a, b in zip(left, right)] 30 31def normalize(vector: list[float]) -> list[float]: 32 scale = math.sqrt(dot(vector, vector)) 33 return [value / scale for value in vector] 34 35def fit_pca(rows: list[list[float]], k: int) -> tuple[list[float], list[list[float]]]: 36 mu = mean_col(rows) 37 centered = [sub(row, mu) for row in rows] 38 n_cols = len(rows[0]) 39 components: list[list[float]] = [] 40 seed = [1.0] * n_cols 41 for _ in range(k): 42 vector = normalize(seed) 43 for _ in range(80): 44 projected = [dot(row, vector) for row in centered] 45 vector = [ 46 sum(centered[i][j] * projected[i] for i in range(len(centered))) 47 for j in range(n_cols) 48 ] 49 for previous in components: 50 coeff = dot(vector, previous) 51 vector = [value - coeff * axis for value, axis in zip(vector, previous)] 52 vector = normalize(vector) 53 components.append(vector) 54 seed = [1.0 if j == len(components) else 0.25 for j in range(n_cols)] 55 return mu, components 56 57def transform(rows: list[list[float]], mu: list[float], components: list[list[float]]) -> list[list[float]]: 58 return [[dot(sub(row, mu), axis) for axis in components] for row in rows] 59 60def nearest_names(rows: list[list[float]]) -> list[str]: 61 neighbors: list[str] = [] 62 for i, left in enumerate(rows): 63 best_j, best_d = -1, float("inf") 64 for j, right in enumerate(rows): 65 if i == j: 66 continue 67 dist = sum((a - b) ** 2 for a, b in zip(left, right)) 68 if dist < best_d: 69 best_j, best_d = j, dist 70 neighbors.append(names[best_j]) 71 return neighbors 72 73def overlap(left: list[str], right: list[str]) -> float: 74 return sum(a == b for a, b in zip(left, right)) / len(left) 75 76original = nearest_names(vectors) 77mu, components = fit_pca(vectors, k=2) 78pca_map = transform(vectors, mu, components) 79pca_neighbors = nearest_names(pca_map) 80pretty_neighbors = nearest_names(pretty_map) 81 82print("Original 1-NN:", list(zip(names, original))) 83print(f"PCA 1-NN overlap: {overlap(original, pca_neighbors):.2f}") 84print(f"Pretty-island 1-NN overlap: {overlap(original, pretty_neighbors):.2f}") 85print(f"Pretty-island neighbor of merge-a: {pretty_neighbors[2]}")
Output
1Original 1-NN: [('rot-a', 'rot-b'), ('rot-b', 'rot-a'), ('merge-a', 'merge-b'), ('merge-b', 'merge-a'), ('access-a', 'access-b'), ('access-b', 'access-a')] 2PCA 1-NN overlap: 1.00 3Pretty-island 1-NN overlap: 0.50 4Pretty-island neighbor of merge-a: rot-b

PCA kept the original pairs on this tiny set because the three themes already sit on high-variance axes. The prettier island map still "looks clustered," but it moved merge-a next to a rotation clause. Local-overlap checks whether a map is useful for investigating nearby cases; it doesn't become a retrieval release gate. That gate remains based on queries and relevant documents.

t-SNE: excellent local pictures, unsafe search coordinates

Start with one question: which points should stay neighbors after the map gets smaller? t-SNE converts high-dimensional neighbor relationships into probabilities pijp_{ij}pij​, creates corresponding low-dimensional probabilities qijq_{ij}qij​ using a heavy-tailed Student-t distribution with one degree of freedom, and minimizes:

DKL(P∥Q)=∑i≠jpijlog⁡pijqijD_{KL}(P \parallel Q) = \sum_{i \ne j} p_{ij} \log \frac{p_{ij}}{q_{ij}}DKL​(P∥Q)=∑i=j​pij​logqij​pij​​

The heavy tail lets moderately distant points spread in the map instead of crowding into the center. This is the central construction in van der Maaten and Hinton's t-SNE paper.[2]Reference 2Visualizing Data using t-SNE.https://jmlr.org/papers/v9/vandermaaten08a.html

A t-SNE visual compares a high-dimensional neighbor matrix P with crowded and optimized 2D states Q; point movement makes local high-probability neighborhoods agree while global gaps stay uninterpretable.
t-SNE optimizes local probability agreement. The final islands help identify nearby records, but the empty space between islands isn't a semantic distance scale.

Because the map is built around neighborhoods, perplexity controls the rough scale considered by t-SNE. Trying several values is useful for investigation. Picking whichever rendering tells the cleanest story isn't a reliable analysis.

UMAP: optimize a weighted neighbor graph

UMAP starts with the same neighbor question but makes the intermediate object explicit: a weighted nearest-neighbor graph. It represents that graph as a fuzzy simplicial set, then optimizes a lower-dimensional graph with a cross-entropy objective.[3]Reference 3UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction.https://arxiv.org/abs/1802.03426 Parameters such as n_neighbors and min_dist change the view.

One layout can expose small pockets of policy confusion while another reveals broader groupings. Read both as views of a chosen graph, not as a new semantic coordinate system.

UMAP turns directed nearest-neighbor scores into a weighted graph and then lays that graph out in 2D; edge pairs attract, sampled non-edges repel, and n_neighbors and min_dist change the visible map.
UMAP's output follows its weighted graph and chosen parameters. Use the map to investigate labeled cases, then verify important neighbors in source vectors.

Common UMAP implementations can place later samples into a fitted map with an approximate transform. That's convenient for a diagnostic dashboard, but it isn't the same as PCA's fixed linear transform. If incoming traffic changes, re-check the map before using it to explain failures.

GoalAppropriate first measurementSuitable methods to test
Inspect clusters or outliersNeighbor overlap plus human reviewPCA, t-SNE, UMAP
Shorten vectors for retrievalRecall@K or nDCG@K plus RAM and latencyNative shortening, PCA, random projection
Reduce memory furtherRecall after compressed search and rerankingScalar, PQ, binary quantization

Random projection: a no-fit serving baseline

Suppose policy documents change often and fitting a corpus-dependent transform is inconvenient. PCA learns from the document corpus; a random projection samples a fixed matrix once and applies it to every document and query:

z=xR,R∈Rd×kz = xR, \qquad R \in \mathbb{R}^{d \times k}z=xR,R∈Rd×k

The Johnson-Lindenstrauss lemma establishes that a finite set of points can be projected into a lower-dimensional space while approximately preserving pairwise distances, given enough target dimensions.[4]Reference 4Extensions of Lipschitz mappings into a Hilbert space.https://web.stanford.edu/class/cs114/readings/JL-Johnson.pdf That bound is a worst-case guarantee, not a claim that a specific 256-dimensional policy index will meet your recall requirement. Save the matrix, then let judged retrieval decide.

A fixed random matrix maps a 1536-dimensional vector to 256 dimensions, keeps example pairwise distances close, and is reused for documents and queries before a Recall@K check.
Random projection is one persisted matrix multiplication, not a fitted corpus model. Its approximate geometry still needs a judged retrieval gate.

The experiment compares original cosine neighbors with a Gaussian random projection. A fixed seed makes the transform reproducible. The Gaussian entries are scaled by 1/k1/\sqrt{k}1/k​, matching the usual construction. Predict the result before running it: fewer bytes can come with a noticeable neighbor loss on this intentionally small target dimension.

random-projection-recall.py
1import math 2import random 3 4rng = random.Random(33) 5documents = [[rng.gauss(0, 1) for _ in range(48)] for _ in range(180)] 6queries = [ 7 [documents[i][j] + rng.gauss(0, 0.03) for j in range(48)] 8 for i in range(12) 9] 10 11def dot(left: list[float], right: list[float]) -> float: 12 return sum(a * b for a, b in zip(left, right)) 13 14def normalize(vector: list[float]) -> list[float]: 15 scale = math.sqrt(dot(vector, vector)) 16 return [value / scale for value in vector] 17 18def neighbors(query_rows: list[list[float]], doc_rows: list[list[float]], k: int = 5) -> list[list[int]]: 19 doc_hat = [normalize(row) for row in doc_rows] 20 ranking: list[list[int]] = [] 21 for query in query_rows: 22 query_hat = normalize(query) 23 scores = [dot(doc, query_hat) for doc in doc_hat] 24 order = sorted(range(len(scores)), key=lambda index: -scores[index]) 25 ranking.append(order[:k]) 26 return ranking 27 28def apply_R(rows: list[list[float]], matrix: list[list[float]]) -> list[list[float]]: 29 n_out = len(matrix[0]) 30 return [ 31 [sum(row[i] * matrix[i][j] for i in range(len(row))) for j in range(n_out)] 32 for row in rows 33 ] 34 35gold = neighbors(queries, documents) 36n_out = 16 37proj_rng = random.Random(7) 38scale = 1.0 / math.sqrt(n_out) 39projection = [[proj_rng.gauss(0, scale) for _ in range(n_out)] for _ in range(48)] 40short_docs = apply_R(documents, projection) 41short_queries = apply_R(queries, projection) 42predicted = neighbors(short_queries, short_docs) 43recall = sum( 44 len(set(left) & set(right)) / len(left) 45 for left, right in zip(gold, predicted) 46) / len(gold) 47 48print(f"Bytes/vector: {48 * 4} -> {n_out * 4}") 49print(f"Neighbor recall@5: {recall:.3f}")
Output
1Bytes/vector: 192 -> 64 2Neighbor recall@5: 0.317

Random projection is worth benchmarking when fitting a data-dependent reducer is inconvenient or expensive. It isn't automatically better or worse than PCA. Let the same labeled retrieval set judge both.

Compression after reduction: quantization

Projection changes how many coordinates a vector has. Quantization changes how precisely those coordinates are stored. You can use the two levers independently or together.

For example, a 256-dimensional float32 vector occupies 256 × 4 = 1,024 bytes. Storing one signed byte per dimension uses 256 bytes. Storing one bit per dimension uses 32 bytes. The smaller representations buy memory by accepting more approximation.

Scalar and binary codes

Scalar quantization maps each value onto an integer grid. Binary quantization records only a sign or threshold result. A compressed index can search cheaply, then rescore a larger candidate set with a more accurate representation. Qdrant's quantization guide documents this as rescore plus oversampling: if oversampling is 2.4 and the limit is 100, the compressed index preselects 240 candidates, then the original vectors rescore that shortlist down to 100.[5]Reference 5Qdrant Quantization Guidehttps://qdrant.tech/documentation/guides/quantization/ The same guide currently treats 4-bit TurboQuant (a random rotation followed by a learned quantizer) as a strong default at 8× compression, and it recommends enabling rescoring for binary codes.

Before trusting compressed candidates, separate the mechanical win from the quality question. The next lab quantizes four normalized vectors to int8 and converts their signs to bits. The bit count is exact; the quality decision still needs retrieval measurements.

scalar-and-binary-codes.py
1vectors = [ 2 [0.90, 0.80, -0.10, -0.20, 0.04, 0.02, -0.05, 0.08], 3 [0.85, 0.74, -0.04, -0.16, 0.01, 0.04, -0.02, 0.05], 4 [-0.02, 0.04, 0.92, 0.81, -0.06, 0.01, 0.02, -0.04], 5 [-0.05, 0.03, 0.88, 0.79, -0.03, 0.00, 0.06, -0.02], 6] 7 8peak = max(abs(value) for row in vectors for value in row) 9scale = 127 / peak 10int8_vectors = [ 11 [max(-128, min(127, int(round(value * scale)))) for value in row] 12 for row in vectors 13] 14binary_vectors = [[value > 0 for value in row] for row in vectors] 15query_bits = binary_vectors[0] 16hamming = [ 17 sum(left != right for left, right in zip(row, query_bits)) 18 for row in binary_vectors 19] 20 21print(f"float32 bytes/vector: {len(vectors[0]) * 4}") 22print(f"int8 bytes/vector: {len(int8_vectors[0])}") 23print(f"binary bits/vector: {len(binary_vectors[0])}") 24print(f"Binary Hamming distances from row 0: {hamming}")
Output
1float32 bytes/vector: 32 2int8 bytes/vector: 8 3binary bits/vector: 8 4Binary Hamming distances from row 0: [0, 0, 6, 7]

Binary codes are tiny, but "same sign" is a much weaker statement than "same semantic evidence." For a policy assistant, retrieve more candidates with a compressed representation and rerank before final evidence selection if benchmarks justify that design. Measure the oversample factor against final recall and end-to-end latency; too small and true neighbors drop before rerank, too large and you pay second-stage cost without quality gain.

Product quantization: code sub-vectors

Binary codes have shown the precision tradeoff. Product Quantization (PQ) changes the storage unit again: it splits a vector into sub-vectors, learns a codebook for each subspace, and stores only the selected centroid IDs.[6]Reference 6Product Quantization for Nearest Neighbor Search.https://dblp.org/rec/journals/pami/JegouDS11 In the common case of 256 possible centroids, each ID fits in one byte. A 128-dimensional float32 vector split into eight subspaces can therefore be represented by eight code bytes, plus shared codebooks.

Product quantization partitions a 128-dimensional vector into eight subspaces, replaces each block with a learned centroid ID, stores eight bytes, and scores a full-precision query through lookup-table sums.
PQ stores eight centroid choices instead of 128 floats. ADC keeps the query uncompressed, builds query-to-centroid lookup tables, and lets each stored byte select one distance to add.

The original PQ paper describes asymmetric distance computation (ADC): keep a query unquantized, precompute its distances to codebook centroids, and score each stored code through table lookups.[6]Reference 6Product Quantization for Nearest Neighbor Search.https://dblp.org/rec/journals/pami/JegouDS11 The miniature implementation uses only two subspaces and four centroids so that every shape remains visible. Predict what shrinks: the stored code gets shorter, while the shared codebooks stay available for scoring.

mini-product-quantization.py
1import random 2 3rng = random.Random(12) 4documents = ( 5 [[rng.gauss(1, 0.08), rng.gauss(1, 0.08), rng.gauss(0, 0.08), rng.gauss(0, 0.08)] for _ in range(20)] 6 + [[rng.gauss(0, 0.08), rng.gauss(0, 0.08), rng.gauss(1, 0.08), rng.gauss(1, 0.08)] for _ in range(20)] 7) 8query = [0.94, 1.02, 0.01, -0.02] 9 10def mean_col(rows: list[list[float]]) -> list[float]: 11 n_rows, n_cols = len(rows), len(rows[0]) 12 return [sum(row[j] for row in rows) / n_rows for j in range(n_cols)] 13 14def kmeans(rows: list[list[float]], k: int, seed: int) -> tuple[list[list[float]], list[int]]: 15 local = random.Random(seed) 16 centroids = [rows[local.randrange(len(rows))][:] ] 17 while len(centroids) < k: 18 weights = [] 19 for row in rows: 20 nearest = min( 21 sum((row[j] - center[j]) ** 2 for j in range(len(row))) 22 for center in centroids 23 ) 24 weights.append(nearest) 25 pick = local.random() * (sum(weights) or 1.0) 26 acc = 0.0 27 chosen = rows[-1] 28 for row, weight in zip(rows, weights): 29 acc += weight 30 if acc >= pick: 31 chosen = row 32 break 33 centroids.append(chosen[:]) 34 labels = [0] * len(rows) 35 for _ in range(15): 36 for i, row in enumerate(rows): 37 labels[i] = min( 38 range(k), 39 key=lambda c: sum((row[j] - centroids[c][j]) ** 2 for j in range(len(row))), 40 ) 41 for cluster in range(k): 42 members = [rows[i] for i, label in enumerate(labels) if label == cluster] 43 if members: 44 centroids[cluster] = mean_col(members) 45 return centroids, labels 46 47subspace_width = 2 48codebooks: list[list[list[float]]] = [] 49codes: list[list[int]] = [] 50for start in range(0, len(documents[0]), subspace_width): 51 block = [row[start:start + subspace_width] for row in documents] 52 centroids, labels = kmeans(block, k=4, seed=start) 53 codebooks.append(centroids) 54 codes.append(labels) 55 56adc_scores = [0.0] * len(documents) 57for subspace, start in enumerate(range(0, len(documents[0]), subspace_width)): 58 query_block = query[start:start + subspace_width] 59 table = [ 60 sum((center[j] - query_block[j]) ** 2 for j in range(subspace_width)) 61 for center in codebooks[subspace] 62 ] 63 for i, code in enumerate(codes[subspace]): 64 adc_scores[i] += table[code] 65 66best = min(range(len(adc_scores)), key=lambda i: adc_scores[i]) 67print(f"Document shape: ({len(documents)}, {len(documents[0])})") 68print(f"Stored code shape: ({len(documents)}, {len(codes)})") 69print(f"Raw bytes/vector: {len(documents[0]) * 8} (float64 in this lab)") 70print(f"Code bytes/vector with <=256 centroids: {len(codes)}") 71print(f"Nearest PQ-coded document group: {'rotation' if best < 20 else 'merge'}")
Output
1Document shape: (40, 4) 2Stored code shape: (40, 2) 3Raw bytes/vector: 32 (float64 in this lab) 4Code bytes/vector with <=256 centroids: 2 5Nearest PQ-coded document group: rotation

PQ isn't an interchangeable synonym for PCA. PCA shortens a coordinate system; PQ encodes sub-vectors with learned IDs. An index may use a shortened embedding and then PQ-code it when memory is tight.

Why can a system use both PCA or native shortening and product quantization?

Answer

They reduce different costs. PCA or native shortening reduces the number of coordinates to compare. PQ stores those coordinates as compact centroid IDs, reducing memory further and enabling lookup-based approximate scoring.

Native shortening: train prefixes to be useful

PCA and random projection operate after an encoder produces its vector. Native shortening moves the decision into training: Matryoshka Representation Learning (MRL) optimizes the model so prefixes of its representation remain useful at several selected lengths.[7]Reference 7Matryoshka Representation Learning.https://arxiv.org/abs/2205.13147

For an embedding eee and prefix lengths in a set M\mathcal{M}M, a simplified view of the original nested objective is a weighted sum of task losses, each computed from a prefix e:me_{:m}e:m​ through its own linear head W(m)W^{(m)}W(m):

min⁡∑m∈Mcm L(W(m)e:m)\min \sum_{m \in \mathcal{M}} c_m \,\mathcal{L}\bigl(W^{(m)} e_{:m}\bigr)min∑m∈M​cm​L(W(m)e:m​)

Unlike PCA, serving a shorter MRL-trained representation doesn't require a separately fitted rotation. It still requires matching document and query lengths and testing retrieval quality. The original paper also notes that intermediate lengths between the trained cuts often interpolate reasonably, but that still isn't a substitute for a product recall gate.

A Matryoshka embedding exposes nested prefixes at 64, 128, 256, and 1024 dimensions; an illustrative recall curve clears its gate at 256 dimensions, which is then shared by documents and queries.
MRL trains selected prefixes to carry task signal. Production then benchmarks supported cuts and applies one chosen length identically to document and query embeddings. The recall curve in the figure is schematic, not a product benchmark.

The lab makes the promise concrete with deliberately ordered vectors. It isn't pretending to train an embedding model: the first coordinates carry policy-theme information, while later coordinates add detail and noise. Truncating a standard, unordered representation wouldn't have this promise. Predict which prefix should recover all three themes before running it.

evaluate-ordered-prefixes.py
1import math 2import random 3 4rng = random.Random(15) 5centers = [ 6 [1.0, 0.9, 0.3, 0.2, 0.0, 0.0, 0.0, 0.0], 7 [0.0, 0.0, 1.0, 0.9, 0.3, 0.2, 0.0, 0.0], 8 [0.0, 0.0, 0.0, 0.0, 1.0, 0.9, 0.3, 0.2], 9] 10documents = [ 11 [center[j] + rng.gauss(0, 0.04) for j in range(8)] 12 for center in centers 13 for _ in range(18) 14] 15queries = [ 16 [center[j] + rng.gauss(0, 0.02) for j in range(8)] 17 for center in centers 18 for _ in range(3) 19] 20 21def dot(left: list[float], right: list[float]) -> float: 22 return sum(a * b for a, b in zip(left, right)) 23 24def normalize(vector: list[float]) -> list[float]: 25 scale = math.sqrt(dot(vector, vector)) 26 return [value / scale for value in vector] 27 28def top3(rows: list[list[float]], corpus: list[list[float]]) -> list[list[int]]: 29 corpus_hat = [normalize(row) for row in corpus] 30 ranking: list[list[int]] = [] 31 for query in rows: 32 query_hat = normalize(query) 33 scores = [dot(doc, query_hat) for doc in corpus_hat] 34 order = sorted(range(len(scores)), key=lambda index: -scores[index]) 35 ranking.append(order[:3]) 36 return ranking 37 38gold = top3(queries, documents) 39for prefix in [2, 4, 8]: 40 shortened = top3( 41 [query[:prefix] for query in queries], 42 [doc[:prefix] for doc in documents], 43 ) 44 recall = sum( 45 len(set(expected) & set(found)) / 3 46 for expected, found in zip(gold, shortened) 47 ) / len(gold) 48 print(f"Prefix {prefix}: recall@3={recall:.3f}")
Output
1Prefix 2: recall@3=0.185 2Prefix 4: recall@3=0.407 3Prefix 8: recall@3=1.000

One hosted API example is OpenAI's text-embedding-3 family. Its documentation lists default lengths of 1,536 for text-embedding-3-small and 3,072 for text-embedding-3-large, and documents a dimensions request parameter for shorter outputs.[8]Reference 8Vector embeddingshttps://developers.openai.com/api/docs/guides/embeddings On MTEB, that page reports that a 256-dimensional text-embedding-3-large vector still outperforms an unshortened 1,536-dimensional text-embedding-ada-002 vector. That's a public benchmark comparison, not a promise about your rotation-ticket queries.

A request shape looks like this:

embedding-request.json
1{ 2 "model": "text-embedding-3-large", 3 "input": "My service-account key is 18 days old. Can I keep using it?", 4 "dimensions": 256 5}

⚠️ Common mistake: If you slice a returned vector yourself, L2-normalize the prefix again. OpenAI's dimensions parameter handles that on their side; a manual cut doesn't.

The provider feature makes generation of shorter vectors convenient. It doesn't decide whether 256 dimensions meet your 18-day-key retrieval requirement.

Choose a deployment experiment

The methods change different parts of the representation. Before choosing one, name the knob you need to turn: coordinates after encoding, coordinates learned into the encoder, or bits used to store each coordinate. The map methods stay on the investigation side.

These methods fall into three serving families and one diagnostic family:

Tool familyWhat changesBest first question
PCA or random projectionNumber of served coordinates after encodingHow much recall survives at a lower dimension?
MRL-style native shorteningNumber of coordinates emitted from a trained modelDoes a supported prefix beat post-hoc alternatives at the same cost?
Scalar, binary, or product quantizationPrecision or code used to store/search vectorsCan compressed candidate search plus reranking fit the RAM budget?
t-SNE or UMAPCoordinates used for inspection mapsWhat failures should an engineer examine next?

The table becomes useful when it turns into a decision someone else can audit. This lab accepts only configurations that meet both a recall floor and a raw-vector memory budget, then chooses the smallest accepted representation. Predict the winner from the two thresholds before reading the output.

select-a-serving-candidate.py
1measurements = [ 2 {"method": "float32-full", "bytes_per_vector": 6144, "recall_at_10": 1.000}, 3 {"method": "native-256", "bytes_per_vector": 1024, "recall_at_10": 0.984}, 4 {"method": "pca-256", "bytes_per_vector": 1024, "recall_at_10": 0.971}, 5 {"method": "pq-16-code", "bytes_per_vector": 16, "recall_at_10": 0.939}, 6] 7 8required_recall = 0.975 9maximum_bytes = 1200 10accepted = [ 11 row for row in measurements 12 if row["recall_at_10"] >= required_recall 13 and row["bytes_per_vector"] <= maximum_bytes 14] 15choice = min(accepted, key=lambda row: row["bytes_per_vector"]) 16 17for row in measurements: 18 status = "pass" if row in accepted else "reject" 19 print(f"{row['method']:<14} recall={row['recall_at_10']:.3f} " 20 f"bytes={row['bytes_per_vector']:<4} {status}") 21print(f"Selected representation: {choice['method']}")
Output
1float32-full recall=1.000 bytes=6144 reject 2native-256 recall=0.984 bytes=1024 pass 3pca-256 recall=0.971 bytes=1024 reject 4pq-16-code recall=0.939 bytes=16 reject 5Selected representation: native-256

The values above are example measurements for the gate, not benchmark results for a real model. A serious report would include:

  1. A frozen set of realistic policy queries and judged clauses.
  2. Recall or nDCG at the candidate count sent to generation or reranking.
  3. Index RAM, build time, and query-latency percentiles for each candidate.
  4. A failure list: which queries lost relevant evidence, and what risk that creates.
  5. Re-evaluation when embedding models, policy documents, or traffic distributions change.

An engineer proposes using a visually clean UMAP plot as a 2D production index. What experiment should you request instead?

Answer

Keep UMAP for inspecting data. Request a serving benchmark that compares model-supported shortening, PCA, or random projection at plausible dimensions, followed by quantization if memory still binds. Approve only with retrieval-quality and resource measurements.

Carry the decision rule forward

You can now calculate the raw cost of a vector representation, establish cosine-retrieval neighbors before changing it, fit PCA once and reuse its transform, and benchmark random projection as a no-fit alternative. For inspection, t-SNE and UMAP can surface cases without becoming search coordinates. For storage, fewer dimensions, lower precision, PQ codes, and model-supported prefixes such as MRL-style shortening are different levers, so each one goes through the same retrieval gate.

No method wins by name alone. Compression is an experiment with a quality constraint: storage savings are immediate, while semantic safety has to be measured.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.Ten million 1,536-dimensional float32 embeddings use 61.44 GB for the raw vector array. The same count at 256 float32 dimensions uses 10.24 GB. What does this calculation justify?

Correct answer: It justifies calling the candidate a 6.0x raw-vector storage reduction, but not shipping it until retrieval or answer quality is measured on representative queries.

The byte arithmetic measures cost, not semantic loss. A shorter representation may save RAM while dropping the policy chunk needed by a query, so approval also needs Recall@K, nDCG@K, or downstream grounded-answer results.

2.A query has two judged relevant policy chunks, R1 and R2. A candidate index returns R1 and D3 as its top 2 results; R2 appears at rank 3. What is recall@2 for this query?

Correct answer: 0.5, because one of the two judged relevant chunks appears within the first two results.

Recall@K counts judged relevant items found within the first K results, divided by the number of judged relevant items. At K = 2, only R1 is retrieved out of the two relevant chunks, so recall@2 is 1/2.

3.A team will build a cosine-search index from PCA-reduced embeddings. Which pipeline keeps document and query scores comparable?

Correct answer: Fit PCA once on representative documents, save it, transform documents and future queries with it, then L2-normalize the reduced vectors before scoring.

PCA learns a coordinate system, so indexed documents and future queries must use the same persisted transform. When cosine search is implemented as a dot product, the transformed vectors must also be L2-normalized. Normalization can't repair separately fitted or dimensionally mismatched spaces.

4.A 256-dimensional PCA index is fitted once on representative policy documents, and live queries are transformed with the saved PCA model. It keeps 95 percent explained variance, but on judged rotation-ticket queries one relevant chunk drops out of the top 5. What is the release decision?

Correct answer: Do not ship this setting; consistent PCA axes and high variance are useful diagnostics, but the retrieval gate failed for required evidence.

PCA optimizes variance, not relevance. Reusing one fitted transform avoids incomparable coordinate systems, but it doesn't guarantee that the reduced vectors preserve the documents needed for retrieval. Judged retrieval quality remains the release metric.

5.A labeled UMAP dashboard shows rotation-ticket clauses and merge-gate clauses as cleanly separated islands in 2D. An engineer wants to use those two coordinates as the production ANN index to save memory. What experiment should be requested?

Correct answer: Keep UMAP as a diagnostic map, then benchmark serving candidates such as native shortening, PCA, random projection, and possibly quantization using recall or nDCG plus RAM and latency.

UMAP and t-SNE are useful for investigation because they expose local structure and suspicious points to inspect. Their 2D distances distort the original embedding geometry, so a production index needs a serving benchmark on judged queries and resource measurements.

6.An embedding team wants a no-fit serving baseline. It samples a Gaussian random projection with a fixed seed, saves the matrix, and applies it to both documents and queries. What must still be checked before serving that index?

Correct answer: Recall or nDCG on held-out judged queries, using the production index and relevance labels, because a JL bound does not predict product retrieval quality.

A random projection can be applied consistently without fitting on the corpus, but its theoretical distance-preservation guarantee isn't the same as passing a product retrieval benchmark. The saved projection still has to be evaluated on relevant-neighbor survival.

7.After reducing embeddings to 128 coordinates, an index uses PQ with 8 subspaces and at most 256 centroids per subspace. In the common one-byte-ID setup, what is stored per vector?

Correct answer: Eight centroid IDs, with the learned codebooks shared across all stored vectors.

PQ splits each vector into eight subvectors and replaces each subvector with the ID of its nearest learned centroid. With at most 256 centroids, each ID fits in one byte, so the per-vector code is eight bytes while the codebooks are shared. This differs from PCA coordinates and binary sign codes.

8.A team has two embedding options for a 256-dimensional policy index: a provider model whose documentation supports a 256-dimensions output parameter, and a generic 1,536-dimensional model with no prefix-shortening guarantee. Which deployment plan is defensible?

Correct answer: Use the supported 256-dimensional output, or another documented MRL-style prefix, for both documents and queries, then approve only if judged retrieval quality meets the gate.

Native shortening is defensible when the model was trained or documented so selected prefixes remain useful. Arbitrarily truncating an unordered embedding has no such guarantee, and documents and queries must use the same representation length before recall is measured.

9.A policy-retrieval team measured these candidates: full float32 uses 6144 bytes/vector with recall@10 of 1.000; native-256 uses 1024 bytes with recall@10 of 0.984; PCA-256 uses 1024 bytes with recall@10 of 0.971; PQ-16-code uses 16 bytes with recall@10 of 0.939. The release gate requires recall@10 >= 0.975 and bytes/vector <= 1200, then chooses the smallest passing representation. Which candidate is selected?

Correct answer: native-256, because it's the only candidate that satisfies both the recall floor and the byte budget among the small-vector options.

The full vector fails the memory budget, PCA-256 and PQ-16-code fail the recall floor, and native-256 satisfies both gates. The lesson's release logic treats compression as acceptable only when it meets both the quality metric and the resource constraint.

10.An int8 or binary index is small and fast, but relevant documents are lost when only its top 10 candidates are used. Which next experiment directly tests a safer compressed-search design?

Correct answer: Retrieve more candidates with compressed codes, rerank them using a more accurate representation, and measure final recall and latency.

Aggressive codes can cheaply generate candidates but may misorder or omit relevant evidence near the cutoff. Retrieving a larger candidate set and rescoring it with a more accurate representation tests whether recall can be recovered at acceptable latency. Compression alone doesn't make quality loss acceptable.

10 questions remaining.

Next Step
Continue to CoT, ToT & Self-Consistency Prompting

Compact embeddings can put the right policy clause in context. Next you'll treat extra inference-time work as a controller: one reasoning path, several votes, or a bounded tree search over those retrieved facts.

PreviousInstruction Tuning & Chat Templates
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Numerical Linear Algebra.

Trefethen, L. N., & Bau, D. · 1997 · SIAM

https://people.maths.ox.ac.uk/trefethen/books.html

Visualizing Data using t-SNE.

van der Maaten, L. & Hinton, G. · 2008

https://jmlr.org/papers/v9/vandermaaten08a.html

UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction.

McInnes, L., Healy, J., & Melville, J. · 2018

https://arxiv.org/abs/1802.03426

Extensions of Lipschitz mappings into a Hilbert space.

Johnson, W. B. & Lindenstrauss, J. · 1984 · Contemporary Mathematics, 26

https://web.stanford.edu/class/cs114/readings/JL-Johnson.pdf

Qdrant Quantization Guide

Qdrant. · 2024

https://qdrant.tech/documentation/guides/quantization/

Product Quantization for Nearest Neighbor Search.

Jégou, H., Douze, M., & Schmid, C. · 2011 · IEEE TPAMI 2011

https://dblp.org/rec/journals/pami/JegouDS11

Matryoshka Representation Learning.

Kusupati, A., et al. · 2022 · NeurIPS 2022

https://arxiv.org/abs/2205.13147

Vector embeddings

OpenAI · 2024

https://developers.openai.com/api/docs/guides/embeddings

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.