Inference
When vector magnitude changes a cosine retrieval ranking
A raw dot product can favor a longer vector over a better directional match. Reproduce the difference and make the zero-vector policy explicit.
Evidence and testing
Review record: Tensor Almanac automated evidence gate (no independent human review). Example versions, recorded outputs and limitations appear below.
A larger dot product need not mean a closer direction
If a retrieval system is meant to rank by cosine similarity, replacing that score with an unnormalized dot product can change the winner. Cosine divides the dot product by both vector lengths. The scikit-learn documentation gives this definition and explains the equivalence with a linear kernel on L2-normalized data. [1]
The fixture uses query (1, 0) and three candidate rows: (2, 2), (1, 0) and (-1, 0). The first candidate has the largest raw dot product, two. The second points in exactly the query direction and has cosine one, beating the first candidate's cosine of approximately 0.707107. This is an arithmetic counterexample to treating the two rankings as interchangeable.
The appropriate scoring rule depends on the retrieval model and application. This example answers the narrower implementation question: does the calculation actually implement the cosine rule we intended? It does not establish which rule produces more useful search results for a particular embedding model.
References: [1]
Normalize each candidate and the query
For a matrix shaped (candidates, features), np.linalg.norm with axis=1 computes one vector length per row. keepdims=True retains a size-one feature dimension so each row can be divided by its own length. NumPy documents both behaviors. The helper returns a new array rather than modifying the supplied candidate values. [2]
The query is represented as a one-row matrix for normalization, then indexed to a vector. np.dot contracts the candidate matrix's last axis with that vector, producing one score per candidate. This matches the documented matrix/vector sum-product rule. [3]
Run python examples/cosine_ranking_norms.py. Assertions check the hand-derived scores, the different winning rows, unit output norms and unchanged input. A second pass independently multiplies each candidate by a positive factor and the query by another positive factor; the cosine scores remain equal within floating-point tolerance.
Executed with Python 3.12.14 and NumPy 2.3.5 on a dense three-candidate fixture. Exact stdout and source-byte SHA-256 are preserved in private evidence.
"""A bounded dense cosine ranking fixture with explicit invalid-vector rejection."""
import numpy as np
def unit_rows(values):
x = np.asarray(values, dtype=np.float64)
if x.ndim != 2 or 0 in x.shape or not np.isfinite(x).all():
raise ValueError('expected a nonempty finite matrix')
with np.errstate(over='raise', invalid='raise', divide='raise', under='ignore'):
norms = np.linalg.norm(x, axis=1, keepdims=True)
if not np.isfinite(norms).all() or (norms == 0).any():
raise ValueError('expected representable nonzero row norms')
return x / norms
query = np.array([[1., 0.]])
candidates = np.array([[2., 2.], [1., 0.], [-1., 0.]])
original = candidates.copy()
raw = np.dot(candidates, query[0])
normalized = unit_rows(candidates)
cosine = np.dot(normalized, unit_rows(query)[0])
assert np.allclose(raw, [2., 1., -1.])
assert np.allclose(cosine, [1 / np.sqrt(2), 1., -1.])
assert int(raw.argmax()) == 0 and int(cosine.argmax()) == 1
assert np.allclose(np.linalg.norm(normalized, axis=1), 1.)
assert np.array_equal(candidates, original)
rescaled = np.dot(unit_rows(candidates * np.array([[7.], [3.], [2.]])), unit_rows(query * 5)[0])
assert np.allclose(cosine, rescaled)
print('raw dot scores:', raw.tolist())
print('cosine scores:', cosine.round(6).tolist())
print('best row, raw/cosine:', int(raw.argmax()), int(cosine.argmax()))
print('positive rescaling preserves cosine:', bool(np.allclose(cosine, rescaled)))
rejected = 0
for bad in ([[0., 0.]], [[np.nan, 1.]], [[np.inf, 1.]], [[]], [1., 0.]):
try:
unit_rows(bad)
except ValueError:
rejected += 1
assert rejected == 5
print('invalid fixtures rejected:', rejected)
Recorded output
raw dot scores: [2.0, 1.0, -1.0] cosine scores: [0.707107, 1.0, -1.0] best row, raw/cosine: 0 1 positive rescaling preserves cosine: True invalid fixtures rejected: 5
Reject zero vectors before choosing a winner
A zero vector has no direction and makes the cosine formula's denominator zero. This helper rejects it explicitly, along with nonfinite values, empty matrices and inputs that are not two-dimensional. These five cases are exercised by the example. This rejection policy is our choice for the fixture; library behavior may differ.
The calculation uses float64 and ordinary Euclidean norms. Very large finite components can overflow while computing a norm, and very small ones can underflow; it is not a general extreme-range normalization routine. The example raises on overflow and rejects computed zero norms. Candidate and query feature counts must also agree. No sparse index, model inference or retrieval benchmark is included.