Semantic Cache Hit Rate at a Similarity Threshold
Problem Statement
An exact-match cache is useless in front of an agent - no two users phrase a question identically. A semantic cache embeds the query and serves a stored answer when the nearest cached query is close enough. The threshold is the whole design: too high and you never hit, too low and you confidently serve the wrong answer. Measuring the trade-off means replaying a query log and counting.
Background
Queries arrive in order. For each one:
- Compute the cosine similarity against every entry currently in the cache.
- Round each similarity to 6 decimal places before comparing - this keeps the boundary case reproducible.
- Take the highest similarity; ties go to the earliest inserted entry.
- If that best similarity is >= threshold, it is a hit - record which entry served it, and do not insert the query (the cache stores one representative per cluster).
- Otherwise it is a miss - insert this query into the cache and move on.
Cosine similarity of a and b is ∥a∥∥b∥a⋅b​; if either vector is all zeros, define the similarity as 0.0.
Your Task
Implement:
def semantic_cache(queries, threshold):
- queries: list of {"id": str, "vec": list[float]}, in arrival order. All vectors have the same length.
Return:
- hits, misses: ints
- hit_rate: hits / len(queries) rounded to 4 decimal places (0.0 for an empty log)
- hit_map: list of [query_id, serving_entry_id] pairs, in arrival order
- cache_size: number of entries in the cache at the end
Input/Output Format
Returns the five-key dict. Do not use numpy-specific printing anywhere; every float in the output is rounded.
Sample
qs = [{"id": "q1", "vec": [1.0, 0.0]},
{"id": "q2", "vec": [2.0, 0.0]},
{"id": "q3", "vec": [0.0, 1.0]}]
out = semantic_cache(qs, 0.9)
print(out["hits"], out["misses"], out["hit_rate"]) # 1 2 0.3333
print(out["hit_map"]) # [['q2', 'q1']]
q2 points the same way as q1 (similarity 1.0) so it hits; q3 is orthogonal (0.0) and misses.
Example:
qs = [{'id':'q1','vec':[1.0,0.0]},{'id':'q2','vec':[2.0,0.0]},{'id':'q3','vec':[0.0,1.0]}]
out = semantic_cache(qs, 0.9)
print(out['hits'], out['misses'], out['hit_rate'])
print(out['hit_map'])1 2 0.3333 [['q2', 'q1']]
q1 arrives to an empty cache, so it must miss and is inserted. q2 is a scalar multiple of q1, giving cosine similarity 1.0 >= 0.9 - a hit served by q1, and it is not inserted. q3 is orthogonal to q1 (similarity 0.0), so it misses and is inserted. One hit out of three queries is a hit rate of 0.3333.
Constraints:
0 <= len(queries) <= 500; vectors are 1 to 64 dimensions- Similarities are rounded to 6 dp before the threshold comparison
- The comparison is
>=, so a similarity exactly equal to the threshold is a hit - Ties on the best similarity resolve to the earliest inserted cache entry
- A hit never inserts into the cache; a miss always does
hit_rateis rounded to 4 dp and is0.0for an empty log- A zero vector has similarity
0.0with everything
1. Background Knowledge
Semantic caching is a technique used in AI applications to optimize latency and reduce computational costs by storing previously generated responses. Unlike traditional exact-match caching, which relies on identical string keys, semantic caching uses vector embeddings to determine if a new query is "similar enough" to a cached one. This allows the system to serve a relevant answer even if the user's phrasing differs slightly from previous queries. The core metric for this comparison is cosine similarity, which measures the cosine of the angle between two non-zero vectors in an inner product space.
Cosine similarity ranges from −1 (exact opposite) to 1 (exact same direction), with 0 indicating orthogonality (no similarity). In the context of text embeddings, vectors are typically normalized, but raw vectors may vary in magnitude. The formula for cosine similarity between vectors a and b is:
similarity=∥a∥∥b∥a⋅b​where a⋅b is the dot product and ∥a∥ is the Euclidean norm (magnitude). A critical edge case is the zero vector, which has no direction. By convention in this problem, if either vector is all zeros, the similarity is defined as 0.0.
The threshold parameter is the hyperparameter that controls the trade-off between cache hit rate and accuracy. A high threshold requires high similarity, resulting in fewer hits but higher confidence in the served answer. A low threshold increases hits but risks serving irrelevant answers. This problem simulates the evaluation of such a system by replaying a query log and tracking performance metrics.
2. Algorithm Approach
The problem requires simulating a sequential insertion and lookup process. Since the cache state changes dynamically (new entries are added on misses), you cannot pre-compute all similarities. Instead, you must process queries one by one, maintaining the current state of the cache.
The core algorithmic pattern is linear scan with dynamic state update. For each incoming query:
- Iterate through all currently stored entries in the cache.
- Compute the cosine similarity between the incoming query vector and each cached vector.
- Identify the maximum similarity.
- Compare the maximum similarity against the threshold to determine if it is a hit or a miss.
- Update the cache and statistics accordingly.
This approach is essentially an online algorithm where decisions are made based on the current state without knowledge of future queries. The "ties go to the earliest inserted entry" rule implies that the order of insertion matters, so the cache should be maintained as an ordered list (e.g., a Python list) to preserve insertion order.
3. Step-by-Step Strategy
- Initialize State Variables:
- Create an empty list for the cache to store entries. Each entry should retain its id and vec.
- Initialize counters: hits = 0, misses = 0.
- Initialize hit_map as an empty list to store [query_id, serving_entry_id] pairs.
- Process Each Query:
- Loop through each query in the input queries list.
- Extract the current query's id and vec.
- Compute Similarities:
- Initialize max_sim = -1.0 (since cosine similarity is ≥−1) and best_entry_id = None.
- Iterate through each cached_entry in the cache.
- Calculate the cosine similarity between the current query vector and the cached_entry vector.
- Handle the zero-vector edge case: if either vector has a norm of 0, similarity is 0.0.
- Otherwise, compute ∥a∥∥b∥a⋅b​.
- Round the similarity to 6 decimal places using round(sim, 6). This is crucial for reproducibility.
- Determine Best Match:
- If the current sim is strictly greater than max_sim, update max_sim and best_entry_id.
- If sim equals max_sim, do not update. This preserves the "earliest inserted" tie-breaking rule because we iterate in insertion order and only update on strict improvement.
- Evaluate Hit or Miss:
- If max_sim >= threshold:
- Increment hits.
- Append [query_id, best_entry_id] to hit_map.
- Do not add the query to the cache.
- Else:
- Increment misses.
- Append the current query (id and vec) to the cache.
- Final Calculations:
- Calculate hit_rate: if len(queries) == 0, return 0.0. Otherwise, round(hits / len(queries), 4).
- cache_size is len(cache).
- Return the dictionary with hits, misses, hit_rate, hit_map, and cache_size.
4. Common Pitfalls
- Floating-Point Precision: The problem explicitly requires rounding similarities to 6 decimal places before comparison. Failing to do this can lead to incorrect hit/miss decisions due to tiny floating-point errors. Always use round(sim, 6).
- Zero Vector Handling: If a vector is all zeros, its norm is 0, leading to division by zero. You must check for this case explicitly and return 0.0 similarity.
- Tie-Breaking Logic: The rule "ties go to the earliest inserted entry" means you should only update the best match if the new similarity is strictly greater than the current maximum. If you use >=, you will incorrectly update to the later entry.
- Cache Update Timing: On a hit, you must not insert the query into the cache. The cache only grows on misses. Inserting on hits will corrupt the cache size and future similarity calculations.
- Hit Rate Rounding: The final hit_rate must be rounded to 4 decimal places. Ensure you handle the empty query list case separately to avoid division by zero.
- Vector Norm Calculation: Remember that the Euclidean norm is the square root of the sum of squared elements. Do not forget the square root.
5. Time & Space Complexity
- Time Complexity: O(Nâ‹…Mâ‹…D), where N is the number of queries, M is the maximum size of the cache (which is at most N), and D is the dimensionality of the vectors. In the worst case (all misses), the cache grows to size N, and for each query, we compare against all previous entries. Each comparison takes O(D) time to compute the dot product and norms.
- Space Complexity: O(Nâ‹…D) to store the cache. In the worst case, all queries are misses, so we store all N vectors, each of dimension D. The hit_map also takes O(N) space in the worst case (all hits).