A SimHash fingerprint maps weighted tokens into a fixed-width bit vector. Each token's digest adds or subtracts its positive weight at every bit; the sign of each accumulated position chooses the output bit. Similar feature bags often produce nearby fingerprints, though similarity of source incidents is not guaranteed by a particular Hamming threshold. This index cuts each 64-bit fingerprint into equal contiguous bands and posts incident IDs under their band values. A query unions all IDs sharing at least one band, then computes the full XOR bit count to verify Hamming distance. With eight bands, accepting a distance of at most seven guarantees candidate recall for indexed fingerprints inside that distance: fewer than eight differing bits cannot disturb all eight bands. Larger thresholds are rejected because one-band matching would then have false negatives. The model indexes distinct IDs and has no update or delete method.
SimHash bands: find near-duplicate incident fingerprints
Operational case
Incident 47 contains weighted dock, scanner, and timeout tokens; incident 83 contains weather and road tokens. Each queried feature bag finds its matching ID at Hamming distance zero. A modified token bag can yield a small or large distance depending on hashed bit interactions; this is a fingerprint index rather than exact text equality. The returned pair includes the incident ID and verified distance. Bands are only a candidate generator. A candidate with a shared band but excessive full distance is removed, and an incident within seven differing bits cannot be missed by the eight-band index. An empty feature bag and nonpositive weights are rejected instead of producing an arbitrary all-zero fingerprint.
Working Python program
import hashlib
class IncidentFingerprintIndex:
def __init__(self, band_count=8):
if not isinstance(band_count, int) or not 1 <= band_count <= 64 or 64 % band_count:
raise ValueError("band count must divide 64")
self.band_count = band_count
self.band_width = 64 // band_count
self.fingerprints = {}
self.buckets = {}
@staticmethod
def fingerprint(features):
if not features or any(not isinstance(token, str) or not isinstance(weight, int) or weight <= 0
for token, weight in features.items()):
raise ValueError("features need positive integer weights")
balance = [0] * 64
for token, weight in features.items():
digest = int.from_bytes(hashlib.blake2b(token.encode(), digest_size=8).digest(), "little")
for bit in range(64):
balance[bit] += weight if digest & (1 << bit) else -weight
return sum(1 << bit for bit, score in enumerate(balance) if score > 0)
def _bands(self, fingerprint):
mask = (1 << self.band_width) - 1
return [(band, (fingerprint >> (band * self.band_width)) & mask)
for band in range(self.band_count)]
def add(self, incident_id, features):
if incident_id in self.fingerprints:
raise ValueError("incident ID already indexed")
fingerprint = self.fingerprint(features)
self.fingerprints[incident_id] = fingerprint
for bucket in self._bands(fingerprint):
self.buckets.setdefault(bucket, set()).add(incident_id)
def neighbors(self, features, max_distance=7):
if not 0 <= max_distance < self.band_count:
raise ValueError("distance must be below band count for complete candidate recall")
fingerprint = self.fingerprint(features)
candidates = set().union(*(self.buckets.get(bucket, set())
for bucket in self._bands(fingerprint)))
return sorted((incident_id, (fingerprint ^ self.fingerprints[incident_id]).bit_count())
for incident_id in candidates
if (fingerprint ^ self.fingerprints[incident_id]).bit_count() <= max_distance)
if __name__ == "__main__":
index = IncidentFingerprintIndex()
index.add(47, {"dock": 3, "scanner": 2, "timeout": 1})
index.add(83, {"weather": 4, "road": 2})
print(index.neighbors({"dock": 3, "scanner": 2, "timeout": 1}))
print(index.neighbors({"weather": 4, "road": 2}))Output
[(47, 0)]
[(83, 0)]Time, space, and tradeoff
For F tokens and W fixed at 64 bits, fingerprinting performs O(FW) bit-weight updates and keeps O(W) temporary scores. An insertion posts one ID to each of eight band buckets. A query reads eight buckets, deduplicates C candidate IDs, and verifies each by one 64-bit XOR and bit count, costing O(FW + C) under fixed-width word operations and O(C) temporary memory. In the worst case every indexed ID shares a band, so C equals N and the query scans the corpus; the bucket index makes no sublinear worst-case promise. Stored fingerprints and postings use O(N times eight) ID references before Python container overhead. MinHash addresses a different set-similarity signal, while this page verifies Hamming distance of weighted-token fingerprints.
Common Mistakes
- Do not report a shared band as a verified near duplicate.
- Do not use a threshold of eight or more with an eight-band recall claim.
- Do not equate small fingerprint distance with semantic equivalence.
- Do not insert the same incident ID twice without a removal or replacement policy.
Connected lessons
- Hashing
- Data Structures
- MinHash bands: retrieve incident candidates, then check exact overlap
- BK-trees: search incident labels within edit distance
- Vantage-point trees: nearest depots by a metric radius
- Trigram indexes: filter and verify substring candidates
- Count Sketch: estimate signed incident frequencies with row medians
- Static XOR filters: peel a fingerprint membership index
- Projects
- Quizzes
Compare this operation boundary with Adaptive radix trees: grow byte-edge nodes as incident codes branch, Bit-sliced indexes: filter and sum fixed-width sensor readings, Minimal acyclic dictionaries: merge equivalent word suffix states, then complete the audit project and decision quiz.
