Filter 90% of duplicate data streams before they hit LLMs using local vector embeddings + exact hashing. Production-ready batched semantic deduplication.
LLM pipelines are expensive. Every redundant or near-duplicate document you send to an embedding model or a large language model wastes tokens, latency, and money. In high-volume systems—news aggregation, social listening, support ticket ingestion, or RAG pipelines—a surprising percentage of incoming items are semantic duplicates of content that has already been processed.
The most effective first line of defense is local semantic deduplication using vector embeddings. By converting each incoming text into a dense vector and comparing it against a local store of previously seen embeddings, you can filter out the vast majority of near-duplicates before they ever reach the costly LLM stage.
I implemented this pattern on a real-time content ingestion pipeline. After careful threshold tuning and several critical fixes to the original design, we consistently filtered between 85 % and 92 % of near-duplicate items. Downstream LLM token consumption dropped dramatically while the system continued to surface genuinely new information.
Why Semantic Deduplication Beats Exact Matching
Exact string matching or simple cryptographic hashing works perfectly for identical documents, but it fails as soon as the wording changes. Two articles can express the same idea with different sentence structure, synonyms, or minor rephrasing and still be treated as unique by a hash-based system. Semantic embeddings capture meaning rather than surface form, so these near-duplicates are correctly identified and filtered.
Critical Bugs in Naive Implementations
Several subtle but serious issues appear in common first versions of this pattern:
- Unpopulated Hash Cache: If an item is rejected as a semantic duplicate, its exact hash is never stored. The next time the identical string arrives, the system re-encodes it unnecessarily.
- Infinite Memory Leak: When a sliding window prunes old embeddings and texts, the exact-hash set continues to grow forever, permanently blocking documents that left the active window.
- Unvectorized Inference: Calling model.encode() on single strings inside a loop incurs massive framework overhead. Batching yields significantly higher throughput.
Production-Ready Batched Implementation
Here is a corrected and vectorized pipeline that addresses all three issues:
import hashlib
import time
from typing import List, Tuple
import numpy as np
from sentence_transformers import SentenceTransformer
class VectorizedDeduplicator:
def __init__(
self,
model_name: str = "all-MiniLM-L6-v2",
similarity_threshold: float = 0.88,
max_items: int = 50_000
):
self.model = SentenceTransformer(model_name)
self.threshold = similarity_threshold
self.max_items = max_items
self.embeddings_list: List[np.ndarray] = []
self.history: List[Tuple[str, str, float]] = [] # (hash, text, timestamp)
self.exact_hashes: set = set()
def _exact_hash(self, text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def process_batch(self, items: List[str]) -> List[str]:
unique_out = []
candidates_to_encode = []
candidate_metadata = []
# 1. Fast O(1) Exact Hash Pre-Filter
for text in items:
text_hash = self._exact_hash(text)
if text_hash in self.exact_hashes:
continue
candidates_to_encode.append(text)
candidate_metadata.append(text_hash)
if not candidates_to_encode:
return []
# 2. Vectorized Batch Inference
embeddings = self.model.encode(
candidates_to_encode,
normalize_embeddings=True,
batch_size=32,
show_progress_bar=False
)
# 3. Matrix Similarity Check against active window
for text_hash, text, emb in zip(candidate_metadata, candidates_to_encode, embeddings):
is_duplicate = False
if self.embeddings_list:
matrix = np.array(self.embeddings_list)
similarities = matrix @ emb
if float(np.max(similarities)) >= self.threshold:
is_duplicate = True
# Always record the hash (even on semantic match) to prevent re-encoding
self.exact_hashes.add(text_hash)
if not is_duplicate:
self.embeddings_list.append(emb)
self.history.append((text_hash, text, time.time()))
unique_out.append(text)
# Maintain sliding window and keep hash set in lockstep
if len(self.history) > self.max_items:
old_hash, _, _ = self.history.pop(0)
self.embeddings_list.pop(0)
self.exact_hashes.discard(old_hash)
return unique_outMathematics of L2-Normalized Dot Products
When embeddings are L2-normalized (∥ e ∥ 2 = 1), cosine similarity simplifies to a single matrix-vector multiplication:
Similarity( E window , v candidate ) = E window ⋅ v candidate T ≥ Ï„
Where is the matrix of active window embeddings, is the incoming normalized vector, and is the decision threshold (e.g., 0.88). Because the embeddings remain normalized, the multiplication executes inside highly optimized BLAS/LAPACK routines at near-hardware speed.
E_{\text{window}} \in \mathbb{R}^{N \times D}v_{\text{candidate}} \in \mathbb{R}^{D}\tauKey Design Decisions
- Exact hashes are always recorded, even when an item is rejected as a semantic duplicate. This prevents repeated encoding of the same string.
- The sliding window prunes embeddings, history, and exact hashes together, eliminating the memory leak.
- Batch encoding removes the per-item framework overhead that dominates single-item inference.
Scaling Notes
The pure NumPy approach works well up to a few tens of thousands of items. Beyond that, replace the list-of-vectors with a proper vector index (FAISS, Qdrant, Chroma, or LanceDB) for both memory efficiency and sub-linear search time.
When This Pattern Delivers Maximum Value
- News aggregation and content monitoring pipelines
- RAG document ingestion systems
- Customer support ticket streams
- Research paper or patent monitoring
- Any high-volume text feed that eventually reaches paid LLM APIs
Final Thoughts
Semantic deduplication with local embeddings remains one of the highest-ROI optimizations you can place in front of an LLM. By combining an exact-hash pre-filter, single-pass vectorized encoding, and a correctly synchronized sliding window, you eliminate the majority of redundant content before it ever consumes tokens. The result is lower cost, lower latency, and a cleaner signal for the language model.Further technical background can be found in the Sentence-Transformers documentation and the FAISS repository.
Tags
AI & Automation

