All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.index.codec.postingsformat.BloomFilter Maven / Gradle / Ivy

There is a newer version: 8.14.1
Show newest version
/*
 * Licensed to Elastic Search and Shay Banon under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Elastic Search licenses this
 * file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.index.codec.postingsformat;

import com.google.common.math.LongMath;
import com.google.common.primitives.Ints;
import org.apache.lucene.store.DataInput;
import org.apache.lucene.store.DataOutput;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.unit.SizeValue;

import java.io.IOException;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Comparator;

/**
 * A bloom filter. Inspired by Guava bloom filter implementation though with some optimizations.
 */
public class BloomFilter {

    /**
     * A factory that can use different fpp based on size.
     */
    public static class Factory {

        public static final Factory DEFAULT = buildDefault();

        private static Factory buildDefault() {
            // Some numbers:
            // 10k =0.001: 140.4kb  , 10 Hashes
            // 10k =0.01 :  93.6kb  ,  6 Hashes
            // 100k=0.01 : 936.0kb  ,  6 Hashes
            // 100k=0.03 : 712.7kb  ,  5 Hashes
            // 500k=0.01 :   4.5mb  ,  6 Hashes
            // 500k=0.03 :   3.4mb  ,  5 Hashes
            // 500k=0.05 :   2.9mb  ,  4 Hashes
            //   1m=0.01 :   9.1mb  ,  6 Hashes
            //   1m=0.03 :   6.9mb  ,  5 Hashes
            //   1m=0.05 :   5.9mb  ,  4 Hashes
            //   5m=0.01 :  45.7mb  ,  6 Hashes
            //   5m=0.03 :  34.8mb  ,  5 Hashes
            //   5m=0.05 :  29.7mb  ,  4 Hashes
            //  50m=0.01 : 457.0mb  ,  6 Hashes
            //  50m=0.03 : 297.3mb  ,  4 Hashes
            //  50m=0.10 : 228.5mb  ,  3 Hashes
            return buildFromString("10k=0.01,1m=0.03");
        }

        /**
         * Supports just passing fpp, as in "0.01", and also ranges, like "50k=0.01,1m=0.05". If
         * its null, returns {@link #buildDefault()}.
         */
        public static Factory buildFromString(@Nullable String config) {
            if (config == null) {
                return buildDefault();
            }
            String[] sEntries = Strings.splitStringToArray(config, ',');
            if (sEntries.length == 0) {
                if (config.length() > 0) {
                    return new Factory(new Entry[]{new Entry(0, Double.parseDouble(config))});
                }
                return buildDefault();
            }
            Entry[] entries = new Entry[sEntries.length];
            for (int i = 0; i < sEntries.length; i++) {
                int index = sEntries[i].indexOf('=');
                entries[i] = new Entry(
                        (int) SizeValue.parseSizeValue(sEntries[i].substring(0, index).trim()).singles(),
                        Double.parseDouble(sEntries[i].substring(index + 1).trim())
                );
            }
            return new Factory(entries);
        }

        private final Entry[] entries;

        public Factory(Entry[] entries) {
            this.entries = entries;
            // the order is from the upper most expected insertions to the lowest
            Arrays.sort(this.entries, new Comparator() {
                @Override
                public int compare(Entry o1, Entry o2) {
                    return o2.expectedInsertions - o1.expectedInsertions;
                }
            });
        }

        public BloomFilter createFilter(int expectedInsertions) {
            for (Entry entry : entries) {
                if (expectedInsertions > entry.expectedInsertions) {
                    return BloomFilter.create(expectedInsertions, entry.fpp);
                }
            }
            return BloomFilter.create(expectedInsertions, 0.03);
        }

        public static class Entry {
            public final int expectedInsertions;
            public final double fpp;

            Entry(int expectedInsertions, double fpp) {
                this.expectedInsertions = expectedInsertions;
                this.fpp = fpp;
            }
        }
    }

    /**
     * Creates a bloom filter based on the with the expected number
     * of insertions and expected false positive probability.
     *
     * @param expectedInsertions the number of expected insertions to the constructed
     * @param fpp                the desired false positive probability (must be positive and less than 1.0)
     */
    public static BloomFilter create(int expectedInsertions, double fpp) {
        if (expectedInsertions == 0) {
            expectedInsertions = 1;
        }
        /*
         * TODO(user): Put a warning in the javadoc about tiny fpp values,
         * since the resulting size is proportional to -log(p), but there is not
         * much of a point after all, e.g. optimalM(1000, 0.0000000000000001) = 76680
         * which is less that 10kb. Who cares!
         */
        long numBits = optimalNumOfBits(expectedInsertions, fpp);
        int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
        try {
            return new BloomFilter(new BitArray(numBits), numHashFunctions);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
        }
    }

    public static BloomFilter deserialize(DataInput in) throws IOException {
        int version = in.readInt(); // we do nothing with this now..., defaults to 0

        int numLongs = in.readInt();
        long[] data = new long[numLongs];
        for (int i = 0; i < numLongs; i++) {
            data[i] = in.readLong();
        }

        int numberOfHashFunctions = in.readInt();

        int hashType = in.readInt(); // again, nothing to do now...

        return new BloomFilter(new BitArray(data), numberOfHashFunctions);
    }

    public static void serilaize(BloomFilter filter, DataOutput out) throws IOException {
        out.writeInt(0); // version

        BitArray bits = filter.bits;
        out.writeInt(bits.data.length);
        for (long l : bits.data) {
            out.writeLong(l);
        }

        out.writeInt(filter.numHashFunctions);

        out.writeInt(0); // hashType
    }

    /**
     * The bit set of the BloomFilter (not necessarily power of 2!)
     */
    final BitArray bits;
    /**
     * Number of hashes per element
     */
    final int numHashFunctions;

    BloomFilter(BitArray bits, int numHashFunctions) {
        this.bits = bits;
        this.numHashFunctions = numHashFunctions;
    /*
     * This only exists to forbid BFs that cannot use the compact persistent representation.
     * If it ever throws, at a user who was not intending to use that representation, we should
     * reconsider
     */
        if (numHashFunctions > 255) {
            throw new IllegalArgumentException("Currently we don't allow BloomFilters that would use more than 255 hash functions");
        }
    }

    public boolean put(BytesRef value) {
        long hash64 = hash3_x64_128(value.bytes, value.offset, value.length, 0);
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        boolean bitsChanged = false;
        for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
            }
            bitsChanged |= bits.set(nextHash % bits.size());
        }
        return bitsChanged;
    }

    public boolean mightContain(BytesRef value) {
        long hash64 = hash3_x64_128(value.bytes, value.offset, value.length, 0);
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
            }
            if (!bits.get(nextHash % bits.size())) {
                return false;
            }
        }
        return true;
    }

    public int getNumHashFunctions() {
        return this.numHashFunctions;
    }

    public long getSizeInBytes() {
        return bits.size() + 8;
    }

    /**
     * Returns the probability that {@linkplain #mightContain(BytesRef)} will erroneously return
     * {@code true} for an object that has not actually been put in the {@code BloomFilter}.
     * 

*

Ideally, this number should be close to the {@code fpp} parameter * passed in create, or smaller. If it is * significantly higher, it is usually the case that too many elements (more than * expected) have been put in the {@code BloomFilter}, degenerating it. */ public double getExpectedFpp() { // You down with FPP? (Yeah you know me!) Who's down with FPP? (Every last homie!) return Math.pow((double) bits.bitCount() / bits.size(), numHashFunctions); } /* * Cheat sheet: * * m: total bits * n: expected insertions * b: m/n, bits per insertion * p: expected false positive probability * * 1) Optimal k = b * ln2 * 2) p = (1 - e ^ (-kn/m))^k * 3) For optimal k: p = 2 ^ (-k) ~= 0.6185^b * 4) For optimal k: m = -nlnp / ((ln2) ^ 2) */ /** * Computes the optimal k (number of hashes per element inserted in Bloom filter), given the * expected insertions and total number of bits in the Bloom filter. *

* See http://en.wikipedia.org/wiki/File:Bloom_filter_fp_probability.svg for the formula. * * @param n expected insertions (must be positive) * @param m total number of bits in Bloom filter (must be positive) */ static int optimalNumOfHashFunctions(long n, long m) { return Math.max(1, (int) Math.round(m / n * Math.log(2))); } /** * Computes m (total bits of Bloom filter) which is expected to achieve, for the specified * expected insertions, the required false positive probability. *

* See http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives for the formula. * * @param n expected insertions (must be positive) * @param p false positive rate (must be 0 < p < 1) */ static long optimalNumOfBits(long n, double p) { if (p == 0) { p = Double.MIN_VALUE; } return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2))); } // START : MURMUR 3_128 protected static long getblock(byte[] key, int offset, int index) { int i_8 = index << 3; int blockOffset = offset + i_8; return ((long) key[blockOffset + 0] & 0xff) + (((long) key[blockOffset + 1] & 0xff) << 8) + (((long) key[blockOffset + 2] & 0xff) << 16) + (((long) key[blockOffset + 3] & 0xff) << 24) + (((long) key[blockOffset + 4] & 0xff) << 32) + (((long) key[blockOffset + 5] & 0xff) << 40) + (((long) key[blockOffset + 6] & 0xff) << 48) + (((long) key[blockOffset + 7] & 0xff) << 56); } protected static long rotl64(long v, int n) { return ((v << n) | (v >>> (64 - n))); } protected static long fmix(long k) { k ^= k >>> 33; k *= 0xff51afd7ed558ccdL; k ^= k >>> 33; k *= 0xc4ceb9fe1a85ec53L; k ^= k >>> 33; return k; } public static long hash3_x64_128(byte[] key, int offset, int length, long seed) { final int nblocks = length >> 4; // Process as 128-bit blocks. long h1 = seed; long h2 = seed; long c1 = 0x87c37b91114253d5L; long c2 = 0x4cf5ad432745937fL; //---------- // body for (int i = 0; i < nblocks; i++) { long k1 = getblock(key, offset, i * 2 + 0); long k2 = getblock(key, offset, i * 2 + 1); k1 *= c1; k1 = rotl64(k1, 31); k1 *= c2; h1 ^= k1; h1 = rotl64(h1, 27); h1 += h2; h1 = h1 * 5 + 0x52dce729; k2 *= c2; k2 = rotl64(k2, 33); k2 *= c1; h2 ^= k2; h2 = rotl64(h2, 31); h2 += h1; h2 = h2 * 5 + 0x38495ab5; } //---------- // tail // Advance offset to the unprocessed tail of the data. offset += nblocks * 16; long k1 = 0; long k2 = 0; switch (length & 15) { case 15: k2 ^= ((long) key[offset + 14]) << 48; case 14: k2 ^= ((long) key[offset + 13]) << 40; case 13: k2 ^= ((long) key[offset + 12]) << 32; case 12: k2 ^= ((long) key[offset + 11]) << 24; case 11: k2 ^= ((long) key[offset + 10]) << 16; case 10: k2 ^= ((long) key[offset + 9]) << 8; case 9: k2 ^= ((long) key[offset + 8]) << 0; k2 *= c2; k2 = rotl64(k2, 33); k2 *= c1; h2 ^= k2; case 8: k1 ^= ((long) key[offset + 7]) << 56; case 7: k1 ^= ((long) key[offset + 6]) << 48; case 6: k1 ^= ((long) key[offset + 5]) << 40; case 5: k1 ^= ((long) key[offset + 4]) << 32; case 4: k1 ^= ((long) key[offset + 3]) << 24; case 3: k1 ^= ((long) key[offset + 2]) << 16; case 2: k1 ^= ((long) key[offset + 1]) << 8; case 1: k1 ^= ((long) key[offset]); k1 *= c1; k1 = rotl64(k1, 31); k1 *= c2; h1 ^= k1; } //---------- // finalization h1 ^= length; h2 ^= length; h1 += h2; h2 += h1; h1 = fmix(h1); h2 = fmix(h2); h1 += h2; h2 += h1; //return (new long[]{h1, h2}); // SAME AS GUAVA, they take the first long out of the 128bit return h1; } // END: MURMUR 3_128 // Note: We use this instead of java.util.BitSet because we need access to the long[] data field static class BitArray { final long[] data; int bitCount; BitArray(long bits) { this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]); } // Used by serialization BitArray(long[] data) { this.data = data; int bitCount = 0; for (long value : data) { bitCount += Long.bitCount(value); } this.bitCount = bitCount; } /** * Returns true if the bit changed value. */ boolean set(int index) { if (!get(index)) { data[index >> 6] |= (1L << index); bitCount++; return true; } return false; } boolean get(int index) { return (data[index >> 6] & (1L << index)) != 0; } /** * Number of bits */ int size() { return data.length * Long.SIZE; } /** * Number of set bits (1s) */ int bitCount() { return bitCount; } BitArray copy() { return new BitArray(data.clone()); } @Override public boolean equals(Object o) { if (o instanceof BitArray) { BitArray bitArray = (BitArray) o; return Arrays.equals(data, bitArray.data); } return false; } @Override public int hashCode() { return Arrays.hashCode(data); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy