Documentation
¶
Overview ¶
Package fingerprint provides two small content-fingerprinting primitives for near-duplicate / similar-content detection:
- TEXT: a 64-bit Charikar SimHash (Compute / Distance / Similarity) for finding near-duplicate documents. Pure stdlib.
- IMAGES: a 64-bit perceptual hash / pHash (PHash / PHashFromImage, with PHashHex / PHashFromHex helpers) for finding visually-similar images. Uses golang.org/x/image for high-quality downscaling.
Both produce a uint64 whose pairwise Hamming Distance (and the derived Similarity = 1 - distance/64) measures closeness — small distance == similar content.
SimHash (text) ¶
SimHash is a locality-sensitive hash: documents whose tokens substantially overlap produce fingerprints whose XOR has few set bits (small Hamming distance). Similarity == 1 - distance/64, so:
distance <= 3 ≈ 95% similarity (near-identical: whitespace / a word or two) distance <= 9 ≈ 85% similarity (minor edits / template fills — a common cut) distance ~ 28+ ≈ 55% similarity (unrelated prose, near the random baseline)
(Shingling registers a localised edit as a few changed shingles rather than one changed token, so small edits sit slightly higher up the distance scale than a single-token hash would put them.)
We tokenise by Unicode letters/digits (lowercased), drop tokens of length < 2, then group them into overlapping k-word SHINGLES (k = shingleSize). Each shingle is hashed via FNV-1a-64 and fed to Charikar's per-bit accumulator: +1 for every shingle whose hash has bit i set, -1 otherwise. The final bit i of the fingerprint is 1 iff the running sum at position i is positive.
Shingles rather than single words because single-token SimHash over natural-language prose is dominated by the high-frequency stopword distribution (the / and / of / to …), which is near-universal across all English text — so two unrelated novels score ~90% similar and cluster spuriously. A document's k-word phrasing is far more distinctive: unrelated prose drops to ~0.55 (near the random baseline) while genuine near-duplicates stay high.
Compute the fingerprint of each document's text, then pairwise compare via Distance or Similarity. For images, use PHash (see phash.go).
Example (ImagePerceptualHash) ¶
Example_imagePerceptualHash shows that PHashHex renders a 64-bit perceptual hash as a 16-character hex string for storage / comparison.
package main
import (
"fmt"
"github.com/richardwooding/fingerprint"
)
func main() {
// In practice: h, _ := fingerprint.PHash(reader)
hex := fingerprint.PHashHex(0x0f0f0f0f0f0f0f0f)
fmt.Println(len(hex), hex)
}
Output: 16 0f0f0f0f0f0f0f0f
Example (TextNearDuplicate) ¶
Example_textNearDuplicate shows detecting a near-duplicate document: a one-word edit leaves the SimHash fingerprints close (high Similarity), while unrelated prose sits near the random baseline.
package main
import (
"fmt"
"github.com/richardwooding/fingerprint"
)
func main() {
base := "The cartographer unrolled the brittle chart across the captain's table and traced the reef-strewn passage with a steady finger, warning that the southern current would carry any vessel onto the rocks before dawn if the helmsman misjudged the tide by even a quarter hour."
// One word changed (dawn → dusk) — a near-duplicate.
edit := "The cartographer unrolled the brittle chart across the captain's table and traced the reef-strewn passage with a steady finger, warning that the southern current would carry any vessel onto the rocks before dusk if the helmsman misjudged the tide by even a quarter hour."
// Entirely different prose.
other := "Quarterly revenue rose twelve percent as the cloud division expanded its enterprise subscriptions, while the board approved a share buyback and raised guidance for the upcoming fiscal year."
a, b, c := fingerprint.Compute(base), fingerprint.Compute(edit), fingerprint.Compute(other)
fmt.Println(fingerprint.Similarity(a, b) > 0.85) // near-duplicate
fmt.Println(fingerprint.Similarity(a, c) < 0.7) // unrelated
}
Output: true true
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Compute ¶
Compute returns the 64-bit SimHash fingerprint of text. Empty input (or input with no usable tokens) returns 0, which is a legitimate fingerprint — callers can distinguish "no content to fingerprint" via a separate len(body) check.
func Distance ¶
Distance returns the Hamming distance between two SimHash fingerprints — the number of bit positions where they differ. Range is 0 (identical) to 64 (maximally different).
func PHash ¶
PHash returns the 64-bit perceptual hash of the image decoded from r. Returns 0 + an error when decoding fails or the image is unusable (smaller than the grid, animated GIF with no first frame, etc.).
PHash is invariant under (file size, modification time) when the pixels don't change, so the cached value in index.Entry.PHash stays valid for as long as the entry itself validates.
func PHashFromHex ¶
PHashFromHex parses a 16-character hex string back into a uint64. Returns 0 + an error when the string isn't a valid hex pHash. Used by the CEL `image_similar_to(reference_path, threshold)` function to compare against a reference image's pHash without re-decoding the reference image per file.
func PHashFromImage ¶
PHashFromImage is the pure-pixel entry point. Useful for callers that already have a decoded image.Image (tests, in-memory pipelines) and for the CEL reference-image cache where the reference is decoded once and reused across the walk.
func PHashHex ¶
PHashHex formats a 64-bit pHash as a 16-character lowercase hex string (the wire form used in CEL `phash` attribute output and in the index cache's JSON / gob encoding for human inspection).
func Similarity ¶
Similarity returns the SimHash similarity score in [0, 1]:
1.0 → fingerprints are identical 0.5 → uncorrelated (random fingerprints average here) 0.0 → fingerprints differ in every bit
Threshold 0.85 (the issue's default) corresponds to Hamming distance ≤ 9.
Types ¶
type PHashError ¶
type PHashError struct {
// contains filtered or unexported fields
}
PHashError is the error type for phash-specific parse / decode failures. Wraps a static message for cheap allocation.
func (*PHashError) Error ¶
func (e *PHashError) Error() string