"""Exact and near-duplicate fingerprinting for document deduplication. Provides: - SHA-256 exact fingerprint on normalized text - SimHash for near-duplicate detection with configurable threshold - Hamming distance comparison between SimHash values """ from __future__ import annotations import hashlib import re import struct def _normalize_text(text: str) -> str: """Normalize text for fingerprinting. Lowercases, collapses whitespace, strips leading/trailing space. This ensures minor formatting differences don't defeat deduplication. """ text = text.lower() text = re.sub(r"\s+", " ", text) return text.strip() def compute_exact_fingerprint(text: str) -> str: """Compute SHA-256 fingerprint of normalized text. Args: text: Raw document text. Returns: Hex-encoded SHA-256 digest of normalized text. """ normalized = _normalize_text(text) return hashlib.sha256(normalized.encode("utf-8")).hexdigest() def _tokenize(text: str) -> list[str]: """Split normalized text into tokens for SimHash computation.""" normalized = _normalize_text(text) return normalized.split() def _hash_token(token: str) -> int: """Hash a single token to a 64-bit integer using MD5 truncation.""" digest = hashlib.md5(token.encode("utf-8")).digest() # noqa: S324 return struct.unpack(" int: """Compute 64-bit SimHash of text for near-duplicate detection. SimHash produces locality-sensitive fingerprints: similar documents will have SimHash values with low Hamming distance. Args: text: Raw document text. Returns: 64-bit SimHash integer value. """ tokens = _tokenize(text) if not tokens: return 0 # Accumulator for each bit position v = [0] * 64 for token in tokens: token_hash = _hash_token(token) for i in range(64): if token_hash & (1 << i): v[i] += 1 else: v[i] -= 1 # Build final hash from accumulator signs fingerprint = 0 for i in range(64): if v[i] > 0: fingerprint |= 1 << i return fingerprint def hamming_distance(a: int, b: int) -> int: """Compute Hamming distance between two 64-bit SimHash values. Args: a: First SimHash value. b: Second SimHash value. Returns: Number of differing bits (0-64). """ xor = a ^ b # Count set bits (Brian Kernighan's algorithm) distance = 0 while xor: xor &= xor - 1 distance += 1 return distance def is_near_duplicate(fp1: int, fp2: int, threshold: int = 3) -> bool: """Determine if two SimHash fingerprints indicate near-duplicate content. Args: fp1: First SimHash value. fp2: Second SimHash value. threshold: Maximum Hamming distance to consider near-duplicate. Default of 3 is conservative for 64-bit SimHash. Returns: True if the documents are near-duplicates. """ return hamming_distance(fp1, fp2) <= threshold