Skip to content

Repository files navigation

Atulin.MinHash

NuGetLicense: MIT.NET 10codecov

Maximum-performance MinHash library for .NET 10, designed for fast approximate Jaccard similarity estimation over text.

Features

  • Zero-allocation hot pathsSpan<T>, stackalloc, and ref locals throughout
  • SIMD acceleration — Vector256 (AVX2) and Vector128 (SSE2) paths auto-selected at runtime
  • xxHash32 shingle hashing via System.IO.Hashing — excellent avalanche, minimal collisions
  • Mersenne-prime universal hashing with vectorised modulo reduction
  • Thread-safe readsMinHasher and MinHashSignature are safe to share across threads
  • MinHashIndex — in-memory similarity-search index with threshold filtering and sorted results

Installation

dotnet add package Atulin.MinHash

Requires .NET 10.0 or later.


Quick Start

usingMinHash;// 1. Create a hasher (shared, thread-safe)varhasher=newMinHasher(signatureSize:128,shingleSize:3);// 2. Compute signaturesuint[]sigA=hasher.ComputeSignature("the quick brown fox jumps over the lazy dog");uint[]sigB=hasher.ComputeSignature("the quick brown fox leaps over the sleepy cat");// 3. Estimate Jaccard similaritydoublesimilarity=MinHasher.EstimateJaccard(sigA,sigB);Console.WriteLine($"Similarity: {similarity:P1}");// e.g. "Similarity: 62.5%"

Usage

MinHasher — Core Engine

// Default: 128 hash functions, 3-char shingles, fixed seedvarhasher=newMinHasher();// Custom configurationvarhasher=newMinHasher(signatureSize:256,// More functions → higher accuracy, more memoryshingleSize:3,// Character n-gram length; 3–5 works well for most textseed:42);// Deterministic parameter generation

ComputeSignature — allocating overload

uint[]signature=hasher.ComputeSignature("hello world");

ComputeSignatureTo — zero-allocation overload

// Reuse a pre-allocated buffer — no heap allocation in the hot pathuint[]buffer=newuint[hasher.SignatureSize];hasher.ComputeSignatureTo("hello world",buffer);

EstimateJaccard — static, SIMD-accelerated

doublej=MinHasher.EstimateJaccard(sigA,sigB);// 0.0 – 1.0

Both spans must have the same length; an ArgumentException is thrown otherwise.


MinHashSignature — Immutable Wrapper

Wraps a raw uint[] signature to provide an ergonomic, value-type API:

varhasher=newMinHasher(signatureSize:128);MinHashSignaturesigA=new(hasher.ComputeSignature("document one"));MinHashSignaturesigB=new(hasher.ComputeSignature("document two"));doublej=sigA.Jaccard(sigB);Console.WriteLine(sigA.Length);// 128ReadOnlySpan<uint>raw=sigA.Span;

MinHashIndex — Similarity Search

Index a collection of documents and query by approximate Jaccard similarity:

varhasher=newMinHasher(signatureSize:128);varindex=newMinHashIndex(hasher);// Add documentsindex.Add("doc-1","the quick brown fox jumps over the lazy dog");index.Add("doc-2","a fast auburn fox leaps across a sleepy hound");index.Add("doc-3","completely unrelated text about cooking pasta");// Query: returns all entries with similarity ≥ threshold, sorted descendingvarresults=index.Query("quick fox jumps over dog",threshold:0.3);foreach(var(key,similarity)inresults)Console.WriteLine($"{key}: {similarity:P1}");// Example output:// doc-1: 68.8%// doc-2: 35.9%

Note

MinHashIndex is not thread-safe for concurrent writes. Reads (Query) may be parallelised safely once the index is fully populated.


Algorithm

  1. Decompose text into overlapping $k$-character shingles (character n-grams).
  2. Hash each shingle with xxHash32 over its raw UTF-16 bytes.
  3. Apply $n$ universal hash functions: $h_i(x) = (a_i x + b_i) \bmod (2^{31} - 1)$
  4. Compute signature: $\text{Signature}[i] = \min_{s \in S} h_i(s)$
  5. Estimate similarity: $J(A, B) \approx \frac{|{i : \text{sig}_A[i] = \text{sig}_B[i]}|}{n}$

The Mersenne-prime modulo $2^{31} - 1$ is computed with a fast bitwise fold instead of integer division.

Benchmarks

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8246/25H2/2025Update/HudsonValley2)
AMD Ryzen 9 9900X 4.40 GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 10.0.202

Tip

1 ms = 1 000 000 ns

MethodSignatureSizeStringLengthMeanErrorStdDevGen0Allocated
ComputeCharSignature12810010,354.366 ns91.5582 ns76.4552 ns0.0305536 B
ComputeCharSignatureInto12810032,480.913 ns53.7245 ns41.9445 ns--
EstimateJaccard1281005.002 ns0.0766 ns0.0679 ns--
ComputeCharSignature1281000363,994.238 ns5,511.1743 ns5,155.1559 ns-536 B
ComputeCharSignatureInto1281000348,559.600 ns6,804.6557 ns9,084.0207 ns--
EstimateJaccard12810005.523 ns0.1280 ns0.2467 ns--
ComputeCharSignature128100003,723,055.273 ns45,820.0107 ns38,261.7841 ns-536 B
ComputeCharSignatureInto128100003,423,947.377 ns29,347.3944 ns26,015.7007 ns--
EstimateJaccard128100005.267 ns0.0175 ns0.0155 ns--
ComputeCharSignature25610023,017.684 ns316.6331 ns280.6870 ns0.06101048 B
ComputeCharSignatureInto25610062,919.740 ns489.8068 ns434.2009 ns--
EstimateJaccard2561009.709 ns0.0918 ns0.0813 ns--
ComputeCharSignature2561000729,181.595 ns3,801.7468 ns3,556.1563 ns-1048 B
ComputeCharSignatureInto2561000652,213.525 ns2,518.8727 ns2,232.9150 ns--
EstimateJaccard25610009.128 ns0.0546 ns0.0484 ns--
ComputeCharSignature256100007,597,260.365 ns70,214.9017 ns65,679.0627 ns-1048 B
ComputeCharSignatureInto256100006,776,450.060 ns66,512.5836 ns55,541.0197 ns--
EstimateJaccard256100009.594 ns0.1088 ns0.0908 ns--
MethodSignatureSizeWordCountMeanErrorStdDevGen0Allocated
ComputeWordSignature12810012,969.747 ns228.0700 ns262.6458 ns0.15262728 B
ComputeWordSignatureInto12810037,677.623 ns209.5786 ns175.0076 ns0.12212192 B
EstimateJaccard1281005.185 ns0.0237 ns0.0210 ns--
ComputeWordSignature128100033,682.019 ns510.5536 ns398.6066 ns0.24414800 B
ComputeWordSignatureInto128100094,254.856 ns1,865.2124 ns2,358.8967 ns0.24414264 B
EstimateJaccard12810005.442 ns0.1289 ns0.1143 ns--
ComputeWordSignature1281000035,363.723 ns700.3849 ns1,150.7525 ns0.24414800 B
ComputeWordSignatureInto1281000090,928.486 ns592.2256 ns524.9925 ns0.24414264 B
EstimateJaccard128100005.378 ns0.0397 ns0.0352 ns--
ComputeWordSignature25610024,609.826 ns231.8569 ns205.5351 ns0.18313240 B
ComputeWordSignatureInto25610069,685.128 ns1,057.9764 ns937.8686 ns0.12212192 B
EstimateJaccard2561009.560 ns0.0768 ns0.0641 ns--
ComputeWordSignature2561000142,392.912 ns2,790.1497 ns2,609.9077 ns0.24415312 B
ComputeWordSignatureInto2561000170,191.572 ns856.1088 ns714.8896 ns0.24414264 B
EstimateJaccard25610009.072 ns0.0485 ns0.0430 ns--
ComputeWordSignature25610000142,703.622 ns2,524.0600 ns2,237.5134 ns0.24415312 B
ComputeWordSignatureInto25610000175,198.652 ns1,072.4958 ns950.7396 ns0.24414264 B
EstimateJaccard256100009.446 ns0.0554 ns0.0518 ns--

Configuration Guide

ParameterDefaultRecommendation
signatureSize128128 for ~97% accuracy; 256 for ~99% accuracy
shingleSize33–4 for short texts; 5 for longer documents
seed0xDEADBEEFChange only if you need independent hash families

Accuracy vs. size trade-off: Jaccard estimation error is approximately $\frac{1}{\sqrt{n}}$

  • At $n = 128$: error $\approx 8.8%$
  • At $n = 256$: error $\approx 6.25%$

Thread Safety

TypeReadWrite
MinHasher✅ Safe✅ Safe (stateless after construction)
MinHashSignature✅ SafeN/A (immutable)
MinHashIndex✅ Safe❌ Not safe for concurrent Add calls

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages