UNPKG

bloom-filters

Version:

JS implementation of probabilistic data structures: Bloom Filter (and its derived), HyperLogLog, Count-Min Sketch, Top-K and MinHash

244 lines (229 loc) • 7.95 kB
import XXH from 'xxhashjs' import {getDefaultSeed, numberToHex} from '../utils' /** * @typedef {TwoHashes} Two hashes of the same value, as computed by {@link hashTwice}. * @property {number} first - The result of the first hashing function applied to a value * @property {number} second - The result of the second hashing function applied to a value * @memberof Utils */ export interface TwoHashes { first: number second: number } /** * Templated TwoHashes type */ export interface TwoHashesTemplated<T> { first: T second: T } /** * TwoHashes type in number and int format */ export interface TwoHashesIntAndString { int: TwoHashesTemplated<number> string: TwoHashesTemplated<string> } /** * Data type of an hashable value, must be string, ArrayBuffer or Buffer. */ export type HashableInput = string | ArrayBuffer | Buffer export default class Hashing implements Hashing { /** * Apply enhanced Double Hashing to produce a n-hash * @see {@link http://peterd.org/pcd-diss.pdf} s6.5.4 * @param n - The indice of the hash function we want to produce * @param hashA - The result of the first hash function applied to a value. * @param hashB - The result of the second hash function applied to a value. * @param size - The size of the datastructures associated to the hash context (ex: the size of a Bloom Filter) * @return The result of hash_n applied to a value. * @memberof Hashing * @author Thomas Minier * @author Arnaud Grall */ public doubleHashing( n: number, hashA: number, hashB: number, size: number ): number { return Math.abs((hashA + n * hashB + Math.floor((n ** 3 - n) / 6)) % size) } /** * Generate a set of distinct indexes on interval [0, size) using the double hashing technique * For generating efficiently distinct indexes we re-hash after detecting a cycle by changing slightly the seed. * It has the effect of generating faster distinct indexes without loosing entirely the utility of the double hashing. * For small number of indexes it will work perfectly. For a number close to the size, and size very large * Advise: do not generate `size` indexes for a large interval. In practice, size should be equal * to the number of hash functions used and is often low. * * @param element - The element to hash * @param size - the range on which we can generate an index [0, size) = size * @param number - The number of indexes desired * @param seed - The seed used * @return Array<number> * @author Arnaud Grall * @author Simon Woolf (SimonWoolf) */ public getDistinctIndexes( element: HashableInput, size: number, number: number, seed?: number ): Array<number> { if (seed === undefined) { seed = getDefaultSeed() } let n = 0 const indexes: Set<number> = new Set() let hashes = this.hashTwice(element, seed) // let cycle = 0 while (indexes.size < number) { const ind = hashes.first % size if (!indexes.has(ind)) { indexes.add(ind) } hashes.first = (hashes.first + hashes.second) % size hashes.second = (hashes.second + n) % size n++ if (n > size) { // Enhanced double hashing stops cycles of length less than `size` in the case where // size is coprime with the second hash. But you still get cycles of length `size`. // So if we reach there and haven't finished, append a prime to the input and // rehash. seed++ hashes = this.hashTwice(element, seed) } } return [...indexes.values()] } /** * Generate N indexes on range [0, size) * It uses the double hashing technique to generate the indexes. * It hash twice the value only once before generating the indexes. * Warning: you can have a lot of modulo collisions. * @param element - The element to hash * @param size - The range on which we can generate the index, exclusive * @param hashCount - The number of indexes we want * @return An array of indexes on range [0, size) */ public getIndexes( element: HashableInput, size: number, hashCount: number, seed?: number ): Array<number> { if (seed === undefined) { seed = getDefaultSeed() } const arr = [] const hashes = this.hashTwice(element, seed) for (let i = 0; i < hashCount; i++) { arr.push(this.doubleHashing(i, hashes.first, hashes.second, size)) } return arr } /** * @internal * Hash an element of type {@link HashableInput} into {@link Number} * Can be overrided as long as you return a value of type {@link Number} * Don't forget to use the seed when hashing, otherwise if some kind of randomness is in the process * you may have inconsistent behaviors between 2 runs. * @param element * @param seed * @returns A 64bits floating point {@link Number} */ public serialize(element: HashableInput, seed?: number) { if (!seed) { seed = getDefaultSeed() } return Number(XXH.h64(element, seed).toNumber()) } /** * (64-bits only) Hash a value into two values (in hex or integer format) * @param value - The value to hash * @param asInt - (optional) If True, the values will be returned as an integer. Otherwise, as hexadecimal values. * @param seed the seed used for hashing * @return The results of the hash functions applied to the value (in hex or integer) * @author Arnaud Grall & Thomas Minier */ public hashTwice(value: HashableInput, seed?: number): TwoHashes { if (seed === undefined) { seed = getDefaultSeed() } return { first: this.serialize(value, seed + 1), second: this.serialize(value, seed + 2), } } /** * Hash twice an element into their HEX string representations * @param value * @param seed * @returns TwoHashesTemplated<string> */ public hashTwiceAsString( value: HashableInput, seed?: number ): TwoHashesTemplated<string> { const {first, second} = this.hashTwice(value, seed) return { first: numberToHex(first), second: numberToHex(second), } } /** * (64-bits only) Same as hashTwice but return Numbers and String equivalent * @param val the value to hash * @param seed the seed to change when hashing * @return TwoHashesIntAndString * @author Arnaud Grall */ public hashTwiceIntAndString( val: HashableInput, seed?: number ): TwoHashesIntAndString { if (seed === undefined) { seed = getDefaultSeed() } const one = this.hashIntAndString(val, seed + 1) const two = this.hashIntAndString(val, seed + 2) return { int: { first: one.int, second: two.int, }, string: { first: one.string, second: two.string, }, } } /** * Hash an item as an unsigned int * @param elem - Element to hash * @param seed - The hash seed. If its is UINT32 make sure to set the length to 32 * @param length - The length of hashes (defaults to 32 bits) * @return The hash value as an unsigned int * @author Arnaud Grall */ public hashAsInt(elem: HashableInput, seed?: number): number { if (seed === undefined) { seed = getDefaultSeed() } return this.serialize(elem, seed) } /** * Hash an item and return its number and HEX string representation * @param elem - Element to hash * @param seed - The hash seed. If its is UINT32 make sure to set the length to 32 * @param base - The base in which the string will be returned, default: 16 * @param length - The length of hashes (defaults to 32 bits) * @return The item hased as an int and a string * @author Arnaud Grall */ public hashIntAndString(elem: HashableInput, seed?: number) { const hash = this.hashAsInt(elem, seed) return {int: hash, string: numberToHex(hash)} } }