Embeddings and Similarity Search — How Vector Search Works
Imagine a vector's magnitude as its "length" or "intensity." Cosine similarity measures the angle between two vectors, and if one vector has zero length, it points nowhere. Attempting to calculate the angle to a point that doesn't exist (a vector with zero magnitude) creates an undefined mathematical operation, leading to division by zero.
The Setup
You are building a fallback local similarity search for your retrieval agent. You write a standard cosine similarity helper function to rank matching text chunks by comparing query embeddings against your stored document embedding vectors.
What Does This Print?
import math
def cosine_similarity(v1: list[float], v2: list[float]) -> float:
# Manual calculation of dot product divided by magnitudes
dot_product = sum(x * y for x, y in zip(v1, v2))
mag1 = math.sqrt(sum(x * x for x in v1))
mag2 = math.sqrt(sum(x * x for x in v2))
return dot_product / (mag1 * mag2)
# Simulating embeddings. Document 2 is generated from an empty/malformed text chunk
query = [0.15, 0.22, 0.81]
doc1 = [0.12, 0.20, 0.85]
doc2 = [0.0, 0.0, 0.0]
print("Doc 1 Similarity:", cosine_similarity(query, doc1))
print("Doc 2 Similarity:", cosine_similarity(query, doc2))
The Output
The script crashes with a ZeroDivisionError. When an empty chunk or system noise results in an all-zero vector, its magnitude (mag2) computes to 0.0. Dividing the dot product by this zero value causes Python to immediately raise an exception, breaking your indexing or retrieval pipeline.
Why Python Does This
Under the hood, Python's floating-point numbers are represented as IEEE 754 double-precision floats. When performing vector operations inside standard loops, Python must resolve dynamic types, allocate memory for temporary floats, and process exception handling checks on every operation. If a vector has a magnitude of 0.0, the mathematical definition of cosine similarity collapses because the direction of a zero-length vector is undefined. Unlike C libraries or NumPy, which handle these boundary limits by returning NaN or 0.0 depending on configuration, pure Python raises ZeroDivisionError. Additionally, calculating vector mathematical operations using list comprehensions and standard loops is incredibly slow due to the Python interpreter's opcode evaluation overhead. For any performance-critical vector manipulation, vectorization via NumPy or PyTorch should be used, which executes compiled C/C++ loops bypassing Python's VM overhead.
The Fix
import numpy as np
def cosine_similarity_safe(v1: list[float], v2: list[float]) -> float:
# Convert lists to NumPy arrays for vectorized C execution
arr1, arr2 = np.array(v1), np.array(v2)
norm1 = np.linalg.norm(arr1)
norm2 = np.linalg.norm(arr2)
# Prevent division by zero safely without raising exceptions
if norm1 == 0.0 or norm2 == 0.0:
return 0.0
return float(np.dot(arr1, arr2) / (norm1 * norm2))
Explicitly checking for zero magnitudes (e.g., if mag1 == 0 or mag2 == 0: return 0.0) before the division prevents the ZeroDivisionError. This handles the edge case of all-zero vectors, ensuring the function returns a sensible default (e.g., 0.0, indicating no similarity) rather than crashing. Using libraries like NumPy can also implicitly handle these cases more gracefully.
How This Fails in Real Systems
A financial analysis bot ingested user uploaded PDFs. During parsing, some blank pages produced all-zero embedding vectors. The search engine crashed with ZeroDivisionError when a user searched across these files, locking up the background Celery task queue and preventing 500 active users from accessing their processed insights.