This is an automated email from the ASF dual-hosted git repository.

leventov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git


The following commit(s) were added to refs/heads/master by this push:
     new a6d81c0  Adds bloom filter aggregator to 'druid-bloom-filters' 
extension (#6397)
a6d81c0 is described below

commit a6d81c0d1654b28d15d922dbe0d128489978fc89
Author: Clint Wylie <[email protected]>
AuthorDate: Tue Jan 29 05:05:17 2019 -0800

    Adds bloom filter aggregator to 'druid-bloom-filters' extension (#6397)
    
    * blooming aggs
    
    * partially address review
    
    * fix docs
    
    * minor test refactor after rebase
    
    * use copied bloomkfilter
    
    * add ByteBuffer methods to BloomKFilter to allow agg to use in place, 
simplify some things, more tests
    
    * add methods to BloomKFilter to get number of set bits, use in comparator, 
fixes
    
    * more docs
    
    * fix
    
    * fix style
    
    * simplify bloomfilter bytebuffer merge, change methods to allow passing 
buffer offsets
    
    * oof, more fixes
    
    * more sane docs example
    
    * fix it
    
    * do the right thing in the right place
    
    * formatting
    
    * fix
    
    * avoid conflict
    
    * typo fixes, faster comparator, docs for comparator behavior
    
    * unused imports
    
    * use buffer comparator instead of deserializing
    
    * striped readwrite lock for buffer agg, null handling comparator, other 
review changes
    
    * style fixes
    
    * style
    
    * remove sync for now
    
    * oops
    
    * consistency
    
    * inspect runtime shape of selector instead of selector plus, static 
comparator, add inner exception on serde exception
    
    * CardinalityBufferAggregator inspect selectors instead of selectorPluses
    
    * fix style
    
    * refactor away from using ColumnSelectorPlus and 
ColumnSelectorStrategyFactory to instead use specialized aggregators for each 
supported column type, other review comments
    
    * adjustment
    
    * fix teamcity error?
    
    * rename nil aggs to empty, change empty agg constructor signature, add 
comments
    
    * use stringutils base64 stuff to be chill with master
    
    * add aggregate combiner, comment
---
 .../development/extensions-core/bloom-filter.md    | 104 +++-
 .../DerivativeDataSourceManager.java               |   2 +-
 .../materializedview/MaterializedViewUtils.java    |   2 +-
 .../druid/guice/BloomFilterSerializersModule.java  |  12 +-
 .../bloom/BaseBloomFilterAggregator.java           |  69 +++
 .../bloom/BaseBloomFilterBufferAggregator.java     |  66 +--
 .../bloom/BloomFilterAggregateCombiner.java        |  72 +++
 .../bloom/BloomFilterAggregatorFactory.java        | 296 ++++++++++
 .../bloom/BloomFilterMergeAggregator.java          |  57 ++
 .../bloom/BloomFilterMergeAggregatorFactory.java   |  84 +++
 .../bloom/BloomFilterMergeBufferAggregator.java    |  40 ++
 .../query/aggregation/bloom/BloomFilterSerde.java  |  69 +++
 .../bloom/DoubleBloomFilterAggregator.java         |  42 ++
 .../bloom/DoubleBloomFilterBufferAggregator.java   |  44 ++
 .../bloom/EmptyBloomFilterAggregator.java          |  37 ++
 .../bloom/EmptyBloomFilterBufferAggregator.java    |  44 ++
 .../bloom/FloatBloomFilterAggregator.java          |  42 ++
 .../bloom/FloatBloomFilterBufferAggregator.java    |  44 ++
 .../bloom/LongBloomFilterAggregator.java           |  42 ++
 .../bloom/LongBloomFilterBufferAggregator.java     |  44 ++
 .../bloom/StringBloomFilterAggregator.java         |  54 ++
 .../bloom/StringBloomFilterBufferAggregator.java   |  56 ++
 .../apache/druid/query/filter/BloomKFilter.java    | 278 ++++++++-
 .../bloom/BloomFilterAggregatorTest.java           | 656 +++++++++++++++++++++
 .../bloom/BloomFilterGroupByQueryTest.java         | 177 ++++++
 .../druid/query/filter/BloomKFilterTest.java       | 541 +++++++++++++++++
 .../src/test/resources/sample.data.tsv             |  13 +
 .../druid/query/aggregation/AggregatorUtil.java    |   4 +
 .../cardinality/CardinalityBufferAggregator.java   |   5 +-
 29 files changed, 2935 insertions(+), 61 deletions(-)

diff --git a/docs/content/development/extensions-core/bloom-filter.md 
b/docs/content/development/extensions-core/bloom-filter.md
index 3b83ff0..f878e75 100644
--- a/docs/content/development/extensions-core/bloom-filter.md
+++ b/docs/content/development/extensions-core/bloom-filter.md
@@ -24,22 +24,44 @@ title: "Bloom Filter"
 
 # Bloom Filter
 
-Make sure to [include](../../operations/including-extensions.html) 
`druid-bloom-filter` as an extension.
+This extension adds the ability to both construct bloom filters from query 
results, and filter query results by testing 
+against a bloom filter. Make sure to 
[include](../../operations/including-extensions.html) `druid-bloom-filter` as 
an 
+extension.
 
-BloomFilter is a probabilistic data structure for set membership check.
-Following are some characterstics of BloomFilter
+A BloomFilter is a probabilistic data structure for performing a set 
membership check. A bloom filter is a good candidate 
+to use with Druid for cases where an explicit filter is impossible, e.g. 
filtering a query against a set of millions of
+ values.
+ 
+Following are some characteristics of BloomFilters:
 - BloomFilters are highly space efficient when compared to using a HashSet.
-- Because of the probabilistic nature of bloom filter false positive (element 
not present in bloom filter but test() says true) are possible
-- false negatives are not possible (if element is present then test() will 
never say false).
-- The false positive probability is configurable (default: 5%) depending on 
which storage requirement may increase or decrease.
-- Lower the false positive probability greater is the space requirement.
-- Bloom filters are sensitive to number of elements that will be inserted in 
the bloom filter.
-- During the creation of bloom filter expected number of entries must be 
specified.If the number of insertions exceed the specified initial number of 
entries then false positive probability will increase accordingly.
+- Because of the probabilistic nature of bloom filters, false positive results 
are possible (element was not actually 
+inserted into a bloom filter during construction, but `test()` says true)
+- False negatives are not possible (if element is present then `test()` will 
never say false). 
+- The false positive probability of this implementation is currently fixed at 
5%, but increasing the number of entries 
+that the filter can hold can decrease this false positive rate in exchange for 
overall size.
+- Bloom filters are sensitive to number of elements that will be inserted in 
the bloom filter. During the creation of bloom filter expected number of 
entries must be specified. If the number of insertions exceed
+ the specified initial number of entries then false positive probability will 
increase accordingly.
+
+This extension is currently based on 
`org.apache.hive.common.util.BloomKFilter` from `hive-storage-api`. Internally, 
+this implementation uses Murmur3 as the hash algorithm.
+
+To construct a BloomKFilter externally with Java to use as a filter in a Druid 
query:
+
+```java
+BloomKFilter bloomFilter = new BloomKFilter(1500);
+bloomFilter.addString("value 1");
+bloomFilter.addString("value 2");
+bloomFilter.addString("value 3");
+ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+BloomKFilter.serialize(byteArrayOutputStream, bloomFilter);
+String base64Serialized = 
Base64.encodeBase64String(byteArrayOutputStream.toByteArray());
+```
 
-Internally, this implementation of bloom filter uses Murmur3 fast 
non-cryptographic hash algorithm.
+This string can then be used in the native or sql Druid query.
 
-### JSON Representation of Bloom Filter
+## Filtering queries with a Bloom Filter
 
+### JSON Specification of Bloom Filter
 ```json
 {
   "type" : "bloom",
@@ -75,7 +97,6 @@ Bloom filters are supported in SQL via the 
`bloom_filter_test` operator:
 SELECT COUNT(*) FROM druid.foo WHERE bloom_filter_test(<expr>, 
'<serialized_bytes_for_BloomKFilter>')
 ```
 
-
 ### Expression and Virtual Column Support
 
 The bloom filter extension also adds a bloom filter [Druid 
expression](../../misc/math-expr.html) which shares syntax 
@@ -83,4 +104,61 @@ with the SQL operator.
 
 ```sql
 bloom_filter_test(<expr>, '<serialized_bytes_for_BloomKFilter>')
-```
\ No newline at end of file
+```
+
+## Bloom Filter Query Aggregator
+
+Input for a `bloomKFilter` can also be created from a druid query with the 
`bloom` aggregator.
+
+### JSON Specification of Bloom Filter Aggregator
+
+```json
+{
+      "type": "bloom",
+      "name": <output_field_name>,
+      "maxNumEntries": <maximum_number_of_elements_for_BloomKFilter>
+      "field": <dimension_spec>
+    }
+```
+
+|Property                 |Description                   |required?            
               |
+|-------------------------|------------------------------|----------------------------------|
+|`type`                   |Aggregator Type. Should always be `bloom`|yes|
+|`name`                   |Output field name |yes|
+|`field`                  |[DimensionSpec](./../dimensionspecs.html) to add to 
`org.apache.hive.common.util.BloomKFilter` | yes |
+|`maxNumEntries`          |Maximum number of distinct values supported by 
`org.apache.hive.common.util.BloomKFilter`, default `1500`| no |
+
+### Example
+
+```json
+{
+  "queryType": "timeseries",
+  "dataSource": "wikiticker",
+  "intervals": [ "2015-09-12T00:00:00.000/2015-09-13T00:00:00.000" ],
+  "granularity": "day",
+  "aggregations": [
+    {
+      "type": "bloom",
+      "name": "userBloom",
+      "maxNumEntries": 100000,
+      "field": {
+        "type":"default",
+        "dimension":"user",
+        "outputType": "STRING"
+      }
+    }
+  ]
+}
+```
+
+response
+
+```json
+[{"timestamp":"2015-09-12T00:00:00.000Z","result":{"userBloom":"BAAAJhAAAA..."}}]
+```
+
+These values can then be set in the filter specification above. 
+
+Ordering results by a bloom filter aggregator, for example in a TopN query, 
will perform a comparatively expensive 
+linear scan _of the filter itself_ to count the number of set bits as a means 
of approximating how many items have been 
+added to the set. As such, ordering by an alternate aggregation is recommended 
if possible. 
\ No newline at end of file
diff --git 
a/extensions-contrib/materialized-view-selection/src/main/java/org/apache/druid/query/materializedview/DerivativeDataSourceManager.java
 
b/extensions-contrib/materialized-view-selection/src/main/java/org/apache/druid/query/materializedview/DerivativeDataSourceManager.java
index 45ab136..35a5c28 100644
--- 
a/extensions-contrib/materialized-view-selection/src/main/java/org/apache/druid/query/materializedview/DerivativeDataSourceManager.java
+++ 
b/extensions-contrib/materialized-view-selection/src/main/java/org/apache/druid/query/materializedview/DerivativeDataSourceManager.java
@@ -210,7 +210,7 @@ public class DerivativeDataSourceManager
   }
 
   /**
-   * caculate the average data size per segment granularity for a given 
datasource.
+   * calculate the average data size per segment granularity for a given 
datasource.
    * 
    * e.g. for a datasource, there're 5 segments as follows,
    * interval = "2018-04-01/2017-04-02", segment size = 1024 * 1024 * 2
diff --git 
a/extensions-contrib/materialized-view-selection/src/main/java/org/apache/druid/query/materializedview/MaterializedViewUtils.java
 
b/extensions-contrib/materialized-view-selection/src/main/java/org/apache/druid/query/materializedview/MaterializedViewUtils.java
index 92eff78..2e96c94 100644
--- 
a/extensions-contrib/materialized-view-selection/src/main/java/org/apache/druid/query/materializedview/MaterializedViewUtils.java
+++ 
b/extensions-contrib/materialized-view-selection/src/main/java/org/apache/druid/query/materializedview/MaterializedViewUtils.java
@@ -85,7 +85,7 @@ public class MaterializedViewUtils
   }
 
   /**
-   * caculate the intervals which are covered by interval2, but not covered by 
interval1.
+   * calculate the intervals which are covered by interval2, but not covered 
by interval1.
    * result intervals = interval2 - interval1 ∩ interval2
    * e.g. 
    * a list of interval2: ["2018-04-01T00:00:00.000Z/2018-04-02T00:00:00.000Z",
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/guice/BloomFilterSerializersModule.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/guice/BloomFilterSerializersModule.java
index 5162abb..0ac4f72 100644
--- 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/guice/BloomFilterSerializersModule.java
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/guice/BloomFilterSerializersModule.java
@@ -27,9 +27,12 @@ import 
com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 import com.fasterxml.jackson.databind.jsontype.NamedType;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import org.apache.druid.query.aggregation.bloom.BloomFilterAggregatorFactory;
+import org.apache.druid.query.aggregation.bloom.BloomFilterSerde;
 import org.apache.druid.query.filter.BloomDimFilter;
 import org.apache.druid.query.filter.BloomKFilter;
 import org.apache.druid.query.filter.BloomKFilterHolder;
+import org.apache.druid.segment.serde.ComplexMetrics;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -41,10 +44,17 @@ public class BloomFilterSerializersModule extends 
SimpleModule
 
   public BloomFilterSerializersModule()
   {
-    registerSubtypes(new NamedType(BloomDimFilter.class, 
BLOOM_FILTER_TYPE_NAME));
+    registerSubtypes(
+        new NamedType(BloomDimFilter.class, BLOOM_FILTER_TYPE_NAME),
+        new NamedType(BloomFilterAggregatorFactory.class, 
BLOOM_FILTER_TYPE_NAME)
+    );
     addSerializer(BloomKFilter.class, new BloomKFilterSerializer());
     addDeserializer(BloomKFilter.class, new BloomKFilterDeserializer());
     addDeserializer(BloomKFilterHolder.class, new 
BloomKFilterHolderDeserializer());
+
+    if (ComplexMetrics.getSerdeForType(BLOOM_FILTER_TYPE_NAME) == null) {
+      ComplexMetrics.registerSerde(BLOOM_FILTER_TYPE_NAME, new 
BloomFilterSerde());
+    }
   }
 
   private static class BloomKFilterSerializer extends 
StdSerializer<BloomKFilter>
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BaseBloomFilterAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BaseBloomFilterAggregator.java
new file mode 100644
index 0000000..652236b
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BaseBloomFilterAggregator.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.query.aggregation.Aggregator;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseNullableColumnValueSelector;
+
+import javax.annotation.Nullable;
+
+public abstract class BaseBloomFilterAggregator<TSelector extends 
BaseNullableColumnValueSelector> implements Aggregator
+{
+  final BloomKFilter collector;
+  protected final TSelector selector;
+
+  BaseBloomFilterAggregator(TSelector selector, BloomKFilter collector)
+  {
+    this.collector = collector;
+    this.selector = selector;
+  }
+
+  @Nullable
+  @Override
+  public Object get()
+  {
+    return collector;
+  }
+
+  @Override
+  public float getFloat()
+  {
+    throw new UnsupportedOperationException("BloomFilterAggregator does not 
support getFloat()");
+  }
+
+  @Override
+  public long getLong()
+  {
+    throw new UnsupportedOperationException("BloomFilterAggregator does not 
support getLong()");
+  }
+
+  @Override
+  public double getDouble()
+  {
+    throw new UnsupportedOperationException("BloomFilterAggregator does not 
support getDouble()");
+  }
+
+  @Override
+  public void close()
+  {
+    // nothing to close
+  }
+}
diff --git 
a/processing/src/main/java/org/apache/druid/query/aggregation/cardinality/CardinalityBufferAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BaseBloomFilterBufferAggregator.java
similarity index 50%
copy from 
processing/src/main/java/org/apache/druid/query/aggregation/cardinality/CardinalityBufferAggregator.java
copy to 
extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BaseBloomFilterBufferAggregator.java
index abb1577..74def15 100644
--- 
a/processing/src/main/java/org/apache/druid/query/aggregation/cardinality/CardinalityBufferAggregator.java
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BaseBloomFilterBufferAggregator.java
@@ -17,101 +17,85 @@
  * under the License.
  */
 
-package org.apache.druid.query.aggregation.cardinality;
+package org.apache.druid.query.aggregation.bloom;
 
-import org.apache.druid.hll.HyperLogLogCollector;
-import org.apache.druid.query.ColumnSelectorPlus;
 import org.apache.druid.query.aggregation.BufferAggregator;
-import 
org.apache.druid.query.aggregation.cardinality.types.CardinalityAggregatorColumnSelectorStrategy;
+import org.apache.druid.query.filter.BloomKFilter;
 import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector;
+import org.apache.druid.segment.BaseNullableColumnValueSelector;
 
 import java.nio.ByteBuffer;
 
-public class CardinalityBufferAggregator implements BufferAggregator
+public abstract class BaseBloomFilterBufferAggregator<TSelector extends 
BaseNullableColumnValueSelector> implements BufferAggregator
 {
-  private final 
ColumnSelectorPlus<CardinalityAggregatorColumnSelectorStrategy>[] 
selectorPluses;
-  private final boolean byRow;
+  protected final int maxNumEntries;
+  protected final TSelector selector;
 
-  private static final byte[] EMPTY_BYTES = 
HyperLogLogCollector.makeEmptyVersionedByteArray();
-
-  CardinalityBufferAggregator(
-      ColumnSelectorPlus<CardinalityAggregatorColumnSelectorStrategy>[] 
selectorPluses,
-      boolean byRow
-  )
+  BaseBloomFilterBufferAggregator(TSelector selector, int maxNumEntries)
   {
-    this.selectorPluses = selectorPluses;
-    this.byRow = byRow;
+    this.selector = selector;
+    this.maxNumEntries = maxNumEntries;
   }
 
+  abstract void bufferAdd(ByteBuffer buf);
+
   @Override
   public void init(ByteBuffer buf, int position)
   {
     final ByteBuffer mutationBuffer = buf.duplicate();
     mutationBuffer.position(position);
-    mutationBuffer.put(EMPTY_BYTES);
+    BloomKFilter filter = new BloomKFilter(maxNumEntries);
+    BloomKFilter.serialize(mutationBuffer, filter);
   }
 
   @Override
   public void aggregate(ByteBuffer buf, int position)
   {
-    // Save position, limit and restore later instead of allocating a new 
ByteBuffer object
     final int oldPosition = buf.position();
-    final int oldLimit = buf.limit();
-    buf.limit(position + 
HyperLogLogCollector.getLatestNumBytesForDenseStorage());
     buf.position(position);
-
-    try {
-      final HyperLogLogCollector collector = 
HyperLogLogCollector.makeCollector(buf);
-      if (byRow) {
-        CardinalityAggregator.hashRow(selectorPluses, collector);
-      } else {
-        CardinalityAggregator.hashValues(selectorPluses, collector);
-      }
-    }
-    finally {
-      buf.limit(oldLimit);
-      buf.position(oldPosition);
-    }
+    bufferAdd(buf);
+    buf.position(oldPosition);
   }
 
+
   @Override
   public Object get(ByteBuffer buf, int position)
   {
-    ByteBuffer dataCopyBuffer = 
ByteBuffer.allocate(HyperLogLogCollector.getLatestNumBytesForDenseStorage());
     ByteBuffer mutationBuffer = buf.duplicate();
     mutationBuffer.position(position);
-    mutationBuffer.get(dataCopyBuffer.array());
-    return HyperLogLogCollector.makeCollector(dataCopyBuffer);
+    // | k (byte) | numLongs (int) | bitset (long[numLongs]) |
+    int sizeBytes = 1 + Integer.BYTES + (buf.getInt(position + 1) * 
Long.BYTES);
+    mutationBuffer.limit(position + sizeBytes);
+    return mutationBuffer.slice();
   }
 
   @Override
   public float getFloat(ByteBuffer buf, int position)
   {
-    throw new UnsupportedOperationException("CardinalityBufferAggregator does 
not support getFloat()");
+    throw new UnsupportedOperationException("BloomFilterBufferAggregator does 
not support getFloat()");
   }
 
-
   @Override
   public long getLong(ByteBuffer buf, int position)
   {
-    throw new UnsupportedOperationException("CardinalityBufferAggregator does 
not support getLong()");
+    throw new UnsupportedOperationException("BloomFilterBufferAggregator does 
not support getLong()");
   }
 
   @Override
   public double getDouble(ByteBuffer buf, int position)
   {
-    throw new UnsupportedOperationException("CardinalityBufferAggregators does 
not support getDouble()");
+    throw new UnsupportedOperationException("BloomFilterBufferAggregator does 
not support getDouble()");
   }
 
   @Override
   public void close()
   {
-    // no resources to cleanup
+    // nothing to close
   }
 
   @Override
   public void inspectRuntimeShape(RuntimeShapeInspector inspector)
   {
-    inspector.visit("selectorPluses", selectorPluses);
+    inspector.visit("selector", selector);
   }
 }
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregateCombiner.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregateCombiner.java
new file mode 100644
index 0000000..6fc4bf9
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregateCombiner.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.query.aggregation.ObjectAggregateCombiner;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.ColumnValueSelector;
+
+import javax.annotation.Nullable;
+
+public class BloomFilterAggregateCombiner extends 
ObjectAggregateCombiner<BloomKFilter>
+{
+  @Nullable
+  private BloomKFilter combined;
+
+  private final int maxNumEntries;
+
+  public BloomFilterAggregateCombiner(int maxNumEntries)
+  {
+    this.maxNumEntries = maxNumEntries;
+  }
+
+  @Override
+  public void reset(ColumnValueSelector selector)
+  {
+    combined = null;
+    fold(selector);
+  }
+
+  @Override
+  public void fold(ColumnValueSelector selector)
+  {
+    BloomKFilter other = (BloomKFilter) selector.getObject();
+    if (other == null) {
+      return;
+    }
+    if (combined == null) {
+      combined = new BloomKFilter(maxNumEntries);
+    }
+    combined.merge(other);
+  }
+
+  @Nullable
+  @Override
+  public BloomKFilter getObject()
+  {
+    return combined;
+  }
+
+  @Override
+  public Class<? extends BloomKFilter> classOfObject()
+  {
+    return BloomKFilter.class;
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorFactory.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorFactory.java
new file mode 100644
index 0000000..af60135
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorFactory.java
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.guice.BloomFilterSerializersModule;
+import org.apache.druid.java.util.common.IAE;
+import org.apache.druid.java.util.common.RE;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.query.aggregation.AggregateCombiner;
+import org.apache.druid.query.aggregation.Aggregator;
+import org.apache.druid.query.aggregation.AggregatorFactory;
+import org.apache.druid.query.aggregation.AggregatorUtil;
+import org.apache.druid.query.aggregation.BufferAggregator;
+import org.apache.druid.query.cache.CacheKeyBuilder;
+import org.apache.druid.query.dimension.DimensionSpec;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseNullableColumnValueSelector;
+import org.apache.druid.segment.ColumnSelectorFactory;
+import org.apache.druid.segment.NilColumnValueSelector;
+import org.apache.druid.segment.column.ColumnCapabilities;
+import org.apache.druid.segment.column.ValueType;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+public class BloomFilterAggregatorFactory extends AggregatorFactory
+{
+  private static final int DEFAULT_NUM_ENTRIES = 1500;
+
+  private static final Comparator COMPARATOR = Comparator.nullsFirst((o1, o2) 
-> {
+    if (o1 instanceof ByteBuffer && o2 instanceof ByteBuffer) {
+      ByteBuffer buf1 = (ByteBuffer) o1;
+      ByteBuffer buf2 = (ByteBuffer) o2;
+      return Integer.compare(
+          BloomKFilter.getNumSetBits(buf1, buf1.position()),
+          BloomKFilter.getNumSetBits(buf2, buf2.position())
+      );
+    } else if (o1 instanceof BloomKFilter && o2 instanceof BloomKFilter) {
+      BloomKFilter o1f = (BloomKFilter) o1;
+      BloomKFilter o2f = (BloomKFilter) o2;
+      return Integer.compare(o1f.getNumSetBits(), o2f.getNumSetBits());
+    } else {
+      throw new RE("Unable to compare unexpected types [%s]", 
o1.getClass().getName());
+    }
+  });
+
+  private final String name;
+  private final DimensionSpec field;
+  private final int maxNumEntries;
+
+  @JsonCreator
+  public BloomFilterAggregatorFactory(
+      @JsonProperty("name") String name,
+      @JsonProperty("field") final DimensionSpec field,
+      @Nullable @JsonProperty("maxNumEntries") Integer maxNumEntries
+  )
+  {
+    this.name = name;
+    this.field = field;
+    this.maxNumEntries = maxNumEntries != null ? maxNumEntries : 
DEFAULT_NUM_ENTRIES;
+  }
+
+  @Override
+  public Aggregator factorize(ColumnSelectorFactory columnFactory)
+  {
+    BloomKFilter filter = new BloomKFilter(maxNumEntries);
+    ColumnCapabilities capabilities = 
columnFactory.getColumnCapabilities(field.getDimension());
+
+    if (capabilities == null) {
+      BaseNullableColumnValueSelector selector = 
columnFactory.makeColumnValueSelector(field.getDimension());
+      if (selector instanceof NilColumnValueSelector) {
+        // BloomKFilter must be the same size so we cannot use a constant for 
the empty agg
+        return new EmptyBloomFilterAggregator(filter);
+      }
+      throw new IAE(
+          "Cannot create bloom filter buffer aggregator for column selector 
type [%s]",
+          selector.getClass().getName()
+      );
+    }
+    ValueType type = capabilities.getType();
+    switch (type) {
+      case STRING:
+        return new 
StringBloomFilterAggregator(columnFactory.makeDimensionSelector(field), filter);
+      case LONG:
+        return new 
LongBloomFilterAggregator(columnFactory.makeColumnValueSelector(field.getDimension()),
 filter);
+      case FLOAT:
+        return new 
FloatBloomFilterAggregator(columnFactory.makeColumnValueSelector(field.getDimension()),
 filter);
+      case DOUBLE:
+        return new 
DoubleBloomFilterAggregator(columnFactory.makeColumnValueSelector(field.getDimension()),
 filter);
+      default:
+        throw new IAE("Cannot create bloom filter aggregator for invalid 
column type [%s]", type);
+    }
+  }
+
+  @Override
+  public BufferAggregator factorizeBuffered(ColumnSelectorFactory 
columnFactory)
+  {
+    ColumnCapabilities capabilities = 
columnFactory.getColumnCapabilities(field.getDimension());
+
+    if (capabilities == null) {
+      BaseNullableColumnValueSelector selector = 
columnFactory.makeColumnValueSelector(field.getDimension());
+      if (selector instanceof NilColumnValueSelector) {
+        return new EmptyBloomFilterBufferAggregator(maxNumEntries);
+      }
+      throw new IAE(
+          "Cannot create bloom filter buffer aggregator for column selector 
type [%s]",
+          selector.getClass().getName()
+      );
+    }
+
+    ValueType type = capabilities.getType();
+    switch (type) {
+      case STRING:
+        return new 
StringBloomFilterBufferAggregator(columnFactory.makeDimensionSelector(field), 
maxNumEntries);
+      case LONG:
+        return new LongBloomFilterBufferAggregator(
+            columnFactory.makeColumnValueSelector(field.getDimension()), 
maxNumEntries
+        );
+      case FLOAT:
+        return new FloatBloomFilterBufferAggregator(
+            columnFactory.makeColumnValueSelector(field.getDimension()), 
maxNumEntries
+        );
+      case DOUBLE:
+        return new DoubleBloomFilterBufferAggregator(
+            columnFactory.makeColumnValueSelector(field.getDimension()), 
maxNumEntries
+        );
+      default:
+        throw new IAE("Cannot create bloom filter buffer aggregator for 
invalid column type [%s]", type);
+    }
+  }
+
+  @Override
+  public Comparator getComparator()
+  {
+    return COMPARATOR;
+  }
+
+  @Override
+  public Object combine(@Nullable Object lhs, @Nullable Object rhs)
+  {
+    if (rhs == null) {
+      return lhs;
+    }
+    if (lhs == null) {
+      return rhs;
+    }
+    ((BloomKFilter) lhs).merge((BloomKFilter) rhs);
+    return lhs;
+  }
+
+  @Override
+  public AggregateCombiner makeAggregateCombiner()
+  {
+    return new BloomFilterAggregateCombiner(maxNumEntries);
+  }
+
+  @Override
+  public AggregatorFactory getCombiningFactory()
+  {
+    return new BloomFilterMergeAggregatorFactory(name, name, maxNumEntries);
+  }
+
+  @Override
+  public List<AggregatorFactory> getRequiredColumns()
+  {
+    return Collections.singletonList(new BloomFilterAggregatorFactory(name, 
field, maxNumEntries));
+  }
+
+  @Override
+  public Object deserialize(Object object)
+  {
+    if (object instanceof String) {
+      return ByteBuffer.wrap(StringUtils.decodeBase64String((String) object));
+    } else {
+      return object;
+    }
+  }
+
+  @Override
+  public Object finalizeComputation(Object object)
+  {
+    try {
+      if (object instanceof ByteBuffer) {
+        return BloomKFilter.deserialize((ByteBuffer) object);
+      } else if (object instanceof byte[]) {
+        return BloomKFilter.deserialize(ByteBuffer.wrap((byte[]) object));
+      } else {
+        return object;
+      }
+    }
+    catch (IOException ioe) {
+      throw new RuntimeException("Failed to deserialize BloomKFilter", ioe);
+    }
+  }
+
+  @JsonProperty
+  @Override
+  public String getName()
+  {
+    return name;
+  }
+
+  @JsonProperty
+  public DimensionSpec getField()
+  {
+    return field;
+  }
+
+  @JsonProperty
+  public int getMaxNumEntries()
+  {
+    return maxNumEntries;
+  }
+
+  @Override
+  public List<String> requiredFields()
+  {
+    return Collections.singletonList(field.getDimension());
+  }
+
+  @Override
+  public String getTypeName()
+  {
+    return BloomFilterSerializersModule.BLOOM_FILTER_TYPE_NAME;
+  }
+
+  @Override
+  public int getMaxIntermediateSize()
+  {
+    return BloomKFilter.computeSizeBytes(maxNumEntries);
+  }
+
+  @Override
+  public byte[] getCacheKey()
+  {
+    return new CacheKeyBuilder(AggregatorUtil.BLOOM_FILTER_CACHE_TYPE_ID)
+        .appendCacheable(field)
+        .appendInt(maxNumEntries)
+        .build();
+  }
+
+  @Override
+  public boolean equals(Object o)
+  {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    BloomFilterAggregatorFactory that = (BloomFilterAggregatorFactory) o;
+    return maxNumEntries == that.maxNumEntries &&
+           Objects.equals(name, that.name) &&
+           Objects.equals(field, that.field);
+  }
+
+  @Override
+  public int hashCode()
+  {
+    return Objects.hash(name, field, maxNumEntries);
+  }
+
+  @Override
+  public String toString()
+  {
+    return "BloomFilterAggregatorFactory{" +
+           "name='" + name + '\'' +
+           ", field=" + field +
+           ", maxNumEntries=" + maxNumEntries +
+           '}';
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeAggregator.java
new file mode 100644
index 0000000..67d7a70
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeAggregator.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.ColumnValueSelector;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public final class BloomFilterMergeAggregator extends 
BaseBloomFilterAggregator<ColumnValueSelector<Object>>
+{
+  public BloomFilterMergeAggregator(ColumnValueSelector<Object> selector, 
BloomKFilter collector)
+  {
+    super(selector, collector);
+  }
+
+  @Override
+  public void aggregate()
+  {
+    Object other = selector.getObject();
+    if (other != null) {
+      if (other instanceof BloomKFilter) {
+        collector.merge((BloomKFilter) other);
+      } else if (other instanceof ByteBuffer) {
+        // fun fact: because bloom filter agg factory deserialize returns a 
byte buffer to avoid unnecessary serde,
+        // but GroupByQueryEngine (group by v1) ends up trying to merge 
ByteBuffers from buffer aggs with this agg
+        // instead of the BloomFilterBufferMergeAggregator. fun! Also, it 
requires a 'ComplexMetricSerde' to be
+        // registered even for query time only aggs, but then never uses it. 
also fun!
+        try {
+          BloomKFilter otherFilter = BloomKFilter.deserialize((ByteBuffer) 
other);
+          collector.merge(otherFilter);
+        }
+        catch (IOException ioe) {
+          throw new RuntimeException("Failed to deserialize BloomKFilter", 
ioe);
+        }
+      }
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeAggregatorFactory.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeAggregatorFactory.java
new file mode 100644
index 0000000..8dab867
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeAggregatorFactory.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.java.util.common.ISE;
+import org.apache.druid.query.aggregation.Aggregator;
+import org.apache.druid.query.aggregation.AggregatorFactory;
+import org.apache.druid.query.aggregation.AggregatorUtil;
+import org.apache.druid.query.aggregation.BufferAggregator;
+import org.apache.druid.query.cache.CacheKeyBuilder;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseNullableColumnValueSelector;
+import org.apache.druid.segment.ColumnSelectorFactory;
+import org.apache.druid.segment.ColumnValueSelector;
+import org.apache.druid.segment.NilColumnValueSelector;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+
+public class BloomFilterMergeAggregatorFactory extends 
BloomFilterAggregatorFactory
+{
+  private final String fieldName;
+
+  BloomFilterMergeAggregatorFactory(String name, String field, Integer 
maxNumEntries)
+  {
+    super(name, null, maxNumEntries);
+    this.fieldName = field;
+  }
+
+  @Override
+  public Aggregator factorize(final ColumnSelectorFactory metricFactory)
+  {
+    final BaseNullableColumnValueSelector selector = 
metricFactory.makeColumnValueSelector(fieldName);
+    // null columns should be empty bloom filters by this point, so 
encountering a nil column in merge agg is unexpected
+    if (selector instanceof NilColumnValueSelector) {
+      throw new ISE("WTF?! Unexpected NilColumnValueSelector");
+    }
+    return new BloomFilterMergeAggregator((ColumnValueSelector<Object>) 
selector, new BloomKFilter(getMaxNumEntries()));
+  }
+
+  @Override
+  public BufferAggregator factorizeBuffered(final ColumnSelectorFactory 
metricFactory)
+  {
+    final BaseNullableColumnValueSelector selector = 
metricFactory.makeColumnValueSelector(fieldName);
+    // null columns should be empty bloom filters by this point, so 
encountering a nil column in merge agg is unexpected
+    if (selector instanceof NilColumnValueSelector) {
+      throw new ISE("WTF?! Unexpected NilColumnValueSelector");
+    }
+    return new 
BloomFilterMergeBufferAggregator((ColumnValueSelector<ByteBuffer>) selector, 
getMaxNumEntries());
+  }
+
+  @Override
+  public List<AggregatorFactory> getRequiredColumns()
+  {
+    return Collections.singletonList(new 
BloomFilterMergeAggregatorFactory(getName(), fieldName, getMaxNumEntries()));
+  }
+
+  @Override
+  public byte[] getCacheKey()
+  {
+    return new CacheKeyBuilder(AggregatorUtil.BLOOM_FILTER_MERGE_CACHE_TYPE_ID)
+        .appendString(fieldName)
+        .appendInt(getMaxNumEntries())
+        .build();
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeBufferAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeBufferAggregator.java
new file mode 100644
index 0000000..026a23e
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterMergeBufferAggregator.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.ColumnValueSelector;
+
+import java.nio.ByteBuffer;
+
+public final class BloomFilterMergeBufferAggregator extends 
BaseBloomFilterBufferAggregator<ColumnValueSelector<ByteBuffer>>
+{
+  public BloomFilterMergeBufferAggregator(ColumnValueSelector<ByteBuffer> 
selector, int maxNumEntries)
+  {
+    super(selector, maxNumEntries);
+  }
+
+  @Override
+  public void bufferAdd(ByteBuffer buf)
+  {
+    ByteBuffer other = selector.getObject();
+    BloomKFilter.mergeBloomFilterByteBuffers(buf, buf.position(), other, 
other.position());
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterSerde.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterSerde.java
new file mode 100644
index 0000000..227fe70
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterSerde.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.guice.BloomFilterSerializersModule;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.GenericColumnSerializer;
+import org.apache.druid.segment.column.ColumnBuilder;
+import org.apache.druid.segment.data.ObjectStrategy;
+import org.apache.druid.segment.serde.ComplexMetricExtractor;
+import org.apache.druid.segment.serde.ComplexMetricSerde;
+import org.apache.druid.segment.writeout.SegmentWriteOutMedium;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Dummy {@link ComplexMetricSerde} that exists so {@link 
BloomFilterAggregatorFactory} has something to register so
+ * {@link org.apache.druid.query.groupby.GroupByQueryEngine} will work, but 
isn't actually used because bloom filter
+ * aggregators are currently only implemented for use at query time
+ */
+public class BloomFilterSerde extends ComplexMetricSerde
+{
+  @Override
+  public String getTypeName()
+  {
+    return BloomFilterSerializersModule.BLOOM_FILTER_TYPE_NAME;
+  }
+
+  @Override
+  public ComplexMetricExtractor getExtractor()
+  {
+    throw new UnsupportedOperationException("Bloom filter aggregators are 
query-time only");
+  }
+
+  @Override
+  public void deserializeColumn(ByteBuffer byteBuffer, ColumnBuilder 
columnBuilder)
+  {
+    throw new UnsupportedOperationException("Bloom filter aggregators are 
query-time only");
+  }
+
+  @Override
+  public GenericColumnSerializer getSerializer(SegmentWriteOutMedium 
segmentWriteOutMedium, String column)
+  {
+    throw new UnsupportedOperationException("Bloom filter aggregators are 
query-time only");
+  }
+
+  @Override
+  public ObjectStrategy<BloomKFilter> getObjectStrategy()
+  {
+    throw new UnsupportedOperationException("Bloom filter aggregators are 
query-time only");
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/DoubleBloomFilterAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/DoubleBloomFilterAggregator.java
new file mode 100644
index 0000000..dfdae6c
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/DoubleBloomFilterAggregator.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseDoubleColumnValueSelector;
+
+public final class DoubleBloomFilterAggregator extends 
BaseBloomFilterAggregator<BaseDoubleColumnValueSelector>
+{
+  DoubleBloomFilterAggregator(BaseDoubleColumnValueSelector selector, 
BloomKFilter collector)
+  {
+    super(selector, collector);
+  }
+
+  @Override
+  public void aggregate()
+  {
+    if (NullHandling.replaceWithDefault() || !selector.isNull()) {
+      collector.addDouble(selector.getDouble());
+    } else {
+      collector.addBytes(null, 0, 0);
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/DoubleBloomFilterBufferAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/DoubleBloomFilterBufferAggregator.java
new file mode 100644
index 0000000..e84b9fc
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/DoubleBloomFilterBufferAggregator.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseDoubleColumnValueSelector;
+
+import java.nio.ByteBuffer;
+
+public final class DoubleBloomFilterBufferAggregator extends 
BaseBloomFilterBufferAggregator<BaseDoubleColumnValueSelector>
+{
+  DoubleBloomFilterBufferAggregator(BaseDoubleColumnValueSelector selector, 
int maxNumEntries)
+  {
+    super(selector, maxNumEntries);
+  }
+
+  @Override
+  public void bufferAdd(ByteBuffer buf)
+  {
+    if (NullHandling.replaceWithDefault() || !selector.isNull()) {
+      BloomKFilter.addDouble(buf, selector.getDouble());
+    } else {
+      BloomKFilter.addBytes(buf, null, 0, 0);
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/EmptyBloomFilterAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/EmptyBloomFilterAggregator.java
new file mode 100644
index 0000000..57df6f2
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/EmptyBloomFilterAggregator.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.NilColumnValueSelector;
+
+public final class EmptyBloomFilterAggregator extends 
BaseBloomFilterAggregator<NilColumnValueSelector>
+{
+  EmptyBloomFilterAggregator(BloomKFilter collector)
+  {
+    super(NilColumnValueSelector.instance(), collector);
+  }
+
+  @Override
+  public void aggregate()
+  {
+    // nothing to do
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/EmptyBloomFilterBufferAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/EmptyBloomFilterBufferAggregator.java
new file mode 100644
index 0000000..7b6301d
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/EmptyBloomFilterBufferAggregator.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.segment.NilColumnValueSelector;
+
+import java.nio.ByteBuffer;
+
+public final class EmptyBloomFilterBufferAggregator extends 
BaseBloomFilterBufferAggregator<NilColumnValueSelector>
+{
+  EmptyBloomFilterBufferAggregator(int maxNumEntries)
+  {
+    super(NilColumnValueSelector.instance(), maxNumEntries);
+  }
+
+  @Override
+  public void bufferAdd(ByteBuffer buf)
+  {
+    // nothing to do
+  }
+
+  @Override
+  public void aggregate(ByteBuffer buf, int position)
+  {
+    // nothing to do
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/FloatBloomFilterAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/FloatBloomFilterAggregator.java
new file mode 100644
index 0000000..ae53d16
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/FloatBloomFilterAggregator.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseFloatColumnValueSelector;
+
+public final class FloatBloomFilterAggregator extends 
BaseBloomFilterAggregator<BaseFloatColumnValueSelector>
+{
+  FloatBloomFilterAggregator(BaseFloatColumnValueSelector selector, 
BloomKFilter collector)
+  {
+    super(selector, collector);
+  }
+
+  @Override
+  public void aggregate()
+  {
+    if (NullHandling.replaceWithDefault() || !selector.isNull()) {
+      collector.addFloat(selector.getFloat());
+    } else {
+      collector.addBytes(null, 0, 0);
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/FloatBloomFilterBufferAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/FloatBloomFilterBufferAggregator.java
new file mode 100644
index 0000000..27e88d4
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/FloatBloomFilterBufferAggregator.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseFloatColumnValueSelector;
+
+import java.nio.ByteBuffer;
+
+public final class FloatBloomFilterBufferAggregator extends 
BaseBloomFilterBufferAggregator<BaseFloatColumnValueSelector>
+{
+  FloatBloomFilterBufferAggregator(BaseFloatColumnValueSelector selector, int 
maxNumEntries)
+  {
+    super(selector, maxNumEntries);
+  }
+
+  @Override
+  public void bufferAdd(ByteBuffer buf)
+  {
+    if (NullHandling.replaceWithDefault() || !selector.isNull()) {
+      BloomKFilter.addFloat(buf, selector.getFloat());
+    } else {
+      BloomKFilter.addBytes(buf, null, 0, 0);
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/LongBloomFilterAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/LongBloomFilterAggregator.java
new file mode 100644
index 0000000..caa4739
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/LongBloomFilterAggregator.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseLongColumnValueSelector;
+
+public final class LongBloomFilterAggregator extends 
BaseBloomFilterAggregator<BaseLongColumnValueSelector>
+{
+  LongBloomFilterAggregator(BaseLongColumnValueSelector selector, BloomKFilter 
collector)
+  {
+    super(selector, collector);
+  }
+
+  @Override
+  public void aggregate()
+  {
+    if (NullHandling.replaceWithDefault() || !selector.isNull()) {
+      collector.addLong(selector.getLong());
+    } else {
+      collector.addBytes(null, 0, 0);
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/LongBloomFilterBufferAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/LongBloomFilterBufferAggregator.java
new file mode 100644
index 0000000..13a6634
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/LongBloomFilterBufferAggregator.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.BaseLongColumnValueSelector;
+
+import java.nio.ByteBuffer;
+
+public final class LongBloomFilterBufferAggregator extends 
BaseBloomFilterBufferAggregator<BaseLongColumnValueSelector>
+{
+  LongBloomFilterBufferAggregator(BaseLongColumnValueSelector selector, int 
maxNumEntries)
+  {
+    super(selector, maxNumEntries);
+  }
+
+  @Override
+  public void bufferAdd(ByteBuffer buf)
+  {
+    if (NullHandling.replaceWithDefault() || !selector.isNull()) {
+      BloomKFilter.addLong(buf, selector.getLong());
+    } else {
+      BloomKFilter.addBytes(buf, null, 0, 0);
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/StringBloomFilterAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/StringBloomFilterAggregator.java
new file mode 100644
index 0000000..351ef84
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/StringBloomFilterAggregator.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.DimensionSelector;
+
+public final class StringBloomFilterAggregator extends 
BaseBloomFilterAggregator<DimensionSelector>
+{
+  StringBloomFilterAggregator(DimensionSelector selector, BloomKFilter 
collector)
+  {
+    super(selector, collector);
+  }
+
+  @Override
+  public void aggregate()
+  {
+    // note: there might be room for optimization here but behavior must match 
BloomDimFilter implementation
+    if (selector.getRow().size() > 1) {
+      selector.getRow().forEach(v -> {
+        String value = selector.lookupName(v);
+        if (value == null) {
+          collector.addBytes(null, 0, 0);
+        } else {
+          collector.addString(value);
+        }
+      });
+    } else {
+      String value = (String) selector.getObject();
+      if (value == null) {
+        collector.addBytes(null, 0, 0);
+      } else {
+        collector.addString(value);
+      }
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/StringBloomFilterBufferAggregator.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/StringBloomFilterBufferAggregator.java
new file mode 100644
index 0000000..c7c17c9
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/StringBloomFilterBufferAggregator.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.segment.DimensionSelector;
+
+import java.nio.ByteBuffer;
+
+public final class StringBloomFilterBufferAggregator extends 
BaseBloomFilterBufferAggregator<DimensionSelector>
+{
+
+  StringBloomFilterBufferAggregator(DimensionSelector selector, int 
maxNumEntries)
+  {
+    super(selector, maxNumEntries);
+  }
+
+  @Override
+  public void bufferAdd(ByteBuffer buf)
+  {
+    if (selector.getRow().size() > 1) {
+      selector.getRow().forEach(v -> {
+        String value = selector.lookupName(v);
+        if (value == null) {
+          BloomKFilter.addBytes(buf, null, 0, 0);
+        } else {
+          BloomKFilter.addString(buf, value);
+        }
+      });
+    } else {
+      String value = (String) selector.getObject();
+      if (value == null) {
+        BloomKFilter.addBytes(buf, null, 0, 0);
+      } else {
+        BloomKFilter.addString(buf, value);
+      }
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/filter/BloomKFilter.java
 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/filter/BloomKFilter.java
index 2949287..12533a2 100644
--- 
a/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/filter/BloomKFilter.java
+++ 
b/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/filter/BloomKFilter.java
@@ -27,6 +27,8 @@ import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.Arrays;
 
 /**
@@ -38,7 +40,13 @@ import java.util.Arrays;
  * 
https://github.com/apache/hive/commit/87ce36b458350db141c4cb4b6336a9a01796370f#diff-e65fc506757ee058dc951d15a9a526c3L238
  * and this linked issue https://issues.apache.org/jira/browse/HIVE-20101.
  *
- * Todo: remove this and begin using hive-storage-api version again once 
https://issues.apache.org/jira/browse/HIVE-20893 is released
+ * Addtionally, a handful of methods have been added to in situ work with 
BloomKFilters that have been serialized to a
+ * ByteBuffer, e.g. all add and merge methods. Test methods were not added 
because we don't need them.. but would
+ * probably be chill to do so it is symmetrical.
+ *
+ * Todo: remove this and begin using hive-storage-api version again once
+ *  https://issues.apache.org/jira/browse/HIVE-20893 is released and if/when 
static ByteBuffer methods have been merged
+ *  (or alternatively, move them to some sort of utils class)
  *
  * begin copy-pasta:
  *
@@ -62,7 +70,7 @@ public class BloomKFilter
   private static final int DEFAULT_BLOCK_SIZE_BITS = (int) 
(Math.log(DEFAULT_BLOCK_SIZE) / Math.log(2));
   private static final int DEFAULT_BLOCK_OFFSET_MASK = DEFAULT_BLOCK_SIZE - 1;
   private static final int DEFAULT_BIT_OFFSET_MASK = Long.SIZE - 1;
-  private final ThreadLocal<byte[]> BYTE_ARRAY_4 = ThreadLocal.withInitial(() 
-> new byte[4]);
+  private static final ThreadLocal<byte[]> BYTE_ARRAY_4 = 
ThreadLocal.withInitial(() -> new byte[4]);
   private final BitSet bitSet;
   private final int m;
   private final int k;
@@ -172,6 +180,8 @@ public class BloomKFilter
     }
   }
 
+  // custom Druid ByteBuffer methods start here
+
   /**
    * Merges BloomKFilter bf2 into bf1.
    * Assumes 2 BloomKFilters with the same size/hash functions are serialized 
to byte arrays
@@ -210,6 +220,259 @@ public class BloomKFilter
     }
   }
 
+  public static void serialize(ByteBuffer out, BloomKFilter bloomFilter)
+  {
+    serialize(out, out.position(), bloomFilter);
+  }
+
+  /**
+   * Serialize a bloom filter to a ByteBuffer. Does not mutate buffer position.
+   *
+   * @param out         output buffer to write to
+   * @param position    output buffer position
+   * @param bloomFilter BloomKFilter that needs to be seralized
+   */
+  public static void serialize(ByteBuffer out, int position, BloomKFilter 
bloomFilter)
+  {
+    /**
+     * Serialized BloomKFilter format:
+     * 1 byte for the number of hash functions.
+     * 1 big endian int(to match OutputStream) for the number of longs in the 
bitset
+     * big endian longs in the BloomKFilter bitset
+     */
+    ByteBuffer view = out.duplicate().order(ByteOrder.BIG_ENDIAN);
+    view.position(position);
+    view.put((byte) bloomFilter.k);
+    view.putInt(bloomFilter.getBitSet().length);
+    for (long value : bloomFilter.getBitSet()) {
+      view.putLong(value);
+    }
+  }
+
+  public static BloomKFilter deserialize(ByteBuffer in) throws IOException
+  {
+    return deserialize(in, in.position());
+  }
+
+  /**
+   * Deserialize a bloom filter
+   * Read a byte buffer, which was written by {@linkplain 
#serialize(OutputStream, BloomKFilter)} or
+   * {@linkplain #serialize(ByteBuffer, int, BloomKFilter)}
+   * into a {@code BloomKFilter}. Does not mutate buffer position.
+   *
+   * @param in input ByteBuffer
+   *
+   * @return deserialized BloomKFilter
+   */
+  public static BloomKFilter deserialize(ByteBuffer in, int position) throws 
IOException
+  {
+    if (in == null) {
+      throw new IOException("Input stream is null");
+    }
+
+    try {
+      ByteBuffer dataBuffer = in.duplicate().order(ByteOrder.BIG_ENDIAN);
+      dataBuffer.position(position);
+      int numHashFunc = dataBuffer.get();
+      int bitsetArrayLen = dataBuffer.getInt();
+      long[] data = new long[bitsetArrayLen];
+      for (int i = 0; i < bitsetArrayLen; i++) {
+        data[i] = dataBuffer.getLong();
+      }
+      return new BloomKFilter(data, numHashFunc);
+    }
+    catch (RuntimeException e) {
+      throw new IOException("Unable to deserialize BloomKFilter", e);
+    }
+  }
+
+  /**
+   * Merges BloomKFilter bf2Buffer into bf1Buffer in place. Does not mutate 
buffer positions.
+   * Assumes 2 BloomKFilters with the same size/hash functions are serialized 
to ByteBuffers
+   *
+   * @param bf1Buffer
+   * @param bf1Start
+   * @param bf2Buffer
+   * @param bf2Start
+   */
+  public static void mergeBloomFilterByteBuffers(
+      ByteBuffer bf1Buffer,
+      int bf1Start,
+      ByteBuffer bf2Buffer,
+      int bf2Start
+  )
+  {
+    ByteBuffer view1 = bf1Buffer.duplicate().order(ByteOrder.BIG_ENDIAN);
+    ByteBuffer view2 = bf2Buffer.duplicate().order(ByteOrder.BIG_ENDIAN);
+    final int bf1Length = START_OF_SERIALIZED_LONGS + (view1.getInt(1 + 
bf1Start) * Long.BYTES);
+    final int bf2Length = START_OF_SERIALIZED_LONGS + (view2.getInt(1 + 
bf2Start) * Long.BYTES);
+
+    if (bf1Length != bf2Length) {
+      throw new IllegalArgumentException("bf1Length " + bf1Length + " does not 
match bf2Length " + bf2Length);
+    }
+
+    // Validation on the bitset size/3 hash functions.
+    for (int idx = 0; idx < START_OF_SERIALIZED_LONGS; ++idx) {
+      if (view1.get(bf1Start + idx) != view2.get(bf2Start + idx)) {
+        throw new IllegalArgumentException("bf1 NumHashFunctions/NumBits does 
not match bf2");
+      }
+    }
+
+    // Just bitwise-OR the bits together - size/# functions should be the same,
+    // rest of the data is serialized long values for the bitset which are 
supposed to be bitwise-ORed.
+    for (int idx = START_OF_SERIALIZED_LONGS; idx < bf1Length; ++idx) {
+      final int pos1 = bf1Start + idx;
+      final int pos2 = bf2Start + idx;
+      view1.put(pos1, (byte) (view1.get(pos1) | view2.get(pos2)));
+    }
+  }
+
+  /**
+   * ByteBuffer based copy of logic of {@link BloomKFilter#getNumSetBits()}
+   * @param bfBuffer
+   * @param start
+   * @return
+   */
+  public static int getNumSetBits(ByteBuffer bfBuffer, int start)
+  {
+    ByteBuffer view = bfBuffer.duplicate().order(ByteOrder.BIG_ENDIAN);
+    view.position(start);
+    int numLongs = view.getInt(1 + start);
+    int setBits = 0;
+    for (int i = 0, pos = START_OF_SERIALIZED_LONGS + start; i < numLongs; 
i++, pos += Long.BYTES) {
+      setBits += Long.bitCount(view.getLong(pos));
+    }
+    return setBits;
+  }
+
+  /**
+   * Calculate size in bytes of a BloomKFilter for a given number of entries
+   */
+  public static int computeSizeBytes(long maxNumEntries)
+  {
+    // copied from constructor
+    checkArgument(maxNumEntries > 0, "expectedEntries should be > 0");
+    long numBits = optimalNumOfBits(maxNumEntries, DEFAULT_FPP);
+
+    int nLongs = (int) Math.ceil((double) numBits / (double) Long.SIZE);
+    int padLongs = DEFAULT_BLOCK_SIZE - nLongs % DEFAULT_BLOCK_SIZE;
+    return START_OF_SERIALIZED_LONGS + ((nLongs + padLongs) * Long.BYTES);
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#add(byte[])} that adds a 
value to the ByteBuffer in place.
+   */
+  public static void add(ByteBuffer buffer, byte[] val)
+  {
+    addBytes(buffer, val);
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addBytes(byte[], int, int)} 
that adds a value to the ByteBuffer
+   * in place.
+   */
+  public static void addBytes(ByteBuffer buffer, byte[] val, int offset, int 
length)
+  {
+    long hash64 = val == null ? Murmur3.NULL_HASHCODE :
+                  Murmur3.hash64(val, offset, length);
+    addHash(buffer, hash64);
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addBytes(byte[])} that adds 
a value to the ByteBuffer in place.
+   */
+  public static void addBytes(ByteBuffer buffer, byte[] val)
+  {
+    addBytes(buffer, val, 0, val.length);
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addHash(long)} that adds a 
value to the ByteBuffer in place.
+   */
+  public static void addHash(ByteBuffer buffer, long hash64)
+  {
+    final int hash1 = (int) hash64;
+    final int hash2 = (int) (hash64 >>> 32);
+
+    int firstHash = hash1 + hash2;
+    // hashcode should be positive, flip all the bits if it's negative
+    if (firstHash < 0) {
+      firstHash = ~firstHash;
+    }
+
+    ByteBuffer view = buffer.duplicate().order(ByteOrder.BIG_ENDIAN);
+    int startPosition = view.position();
+    int numHashFuncs = view.get(startPosition);
+    int totalBlockCount = view.getInt(startPosition + 1) / DEFAULT_BLOCK_SIZE;
+    // first hash is used to locate start of the block (blockBaseOffset)
+    // subsequent K hashes are used to generate K bits within a block of words
+    final int blockIdx = firstHash % totalBlockCount;
+    final int blockBaseOffset = blockIdx << DEFAULT_BLOCK_SIZE_BITS;
+    for (int i = 1; i <= numHashFuncs; i++) {
+      int combinedHash = hash1 + ((i + 1) * hash2);
+      // hashcode should be positive, flip all the bits if it's negative
+      if (combinedHash < 0) {
+        combinedHash = ~combinedHash;
+      }
+      // LSB 3 bits is used to locate offset within the block
+      final int absOffset = blockBaseOffset + (combinedHash & 
DEFAULT_BLOCK_OFFSET_MASK);
+      // Next 6 bits are used to locate offset within a long/word
+      final int bitPos = (combinedHash >>> DEFAULT_BLOCK_SIZE_BITS) & 
DEFAULT_BIT_OFFSET_MASK;
+
+      final int bufPos = startPosition + START_OF_SERIALIZED_LONGS + 
(absOffset * Long.BYTES);
+      view.putLong(bufPos, view.getLong(bufPos) | (1L << bitPos));
+    }
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addString(String)} that adds 
a value to the ByteBuffer in place.
+   */
+  public static void addString(ByteBuffer buffer, String val)
+  {
+    addBytes(buffer, StringUtils.toUtf8(val));
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addByte(byte)} that adds a 
value to the ByteBuffer in place.
+   */
+  public static void addByte(ByteBuffer buffer, byte val)
+  {
+    addBytes(buffer, new byte[]{val});
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addInt(int)} that adds a 
value to the ByteBuffer in place.
+   */
+  public static void addInt(ByteBuffer buffer, int val)
+  {
+    addBytes(buffer, intToByteArrayLE(val));
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addLong(long)} that adds a 
value to the ByteBuffer in place.
+   */
+  public static void addLong(ByteBuffer buffer, long val)
+  {
+    addHash(buffer, Murmur3.hash64(val));
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addFloat(float)} that adds a 
value to the ByteBuffer in place.
+   */
+  public static void addFloat(ByteBuffer buffer, float val)
+  {
+    addInt(buffer, Float.floatToIntBits(val));
+  }
+
+  /**
+   * ByteBuffer based copy of {@link BloomKFilter#addDouble(double)}
+   */
+  public static void addDouble(ByteBuffer buffer, double val)
+  {
+    addLong(buffer, Double.doubleToLongBits(val));
+  }
+  // custom Druid ByteBuffer methods end here
+
   public void add(byte[] val)
   {
     addBytes(val);
@@ -381,7 +644,7 @@ public class BloomKFilter
     return testLong(Double.doubleToLongBits(val));
   }
 
-  private byte[] intToByteArrayLE(int val)
+  private static byte[] intToByteArrayLE(int val)
   {
     byte[] bytes = BYTE_ARRAY_4.get();
     bytes[0] = (byte) (val >> 0);
@@ -401,6 +664,15 @@ public class BloomKFilter
     return bitSet.getData().length * Long.SIZE;
   }
 
+  public int getNumSetBits()
+  {
+    int setCount = 0;
+    for (long datum : bitSet.getData()) {
+      setCount += Long.bitCount(datum);
+    }
+    return setCount;
+  }
+
   public int getNumHashFunctions()
   {
     return k;
diff --git 
a/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorTest.java
 
b/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorTest.java
new file mode 100644
index 0000000..790cf8c
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorTest.java
@@ -0,0 +1,656 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.guice.BloomFilterExtensionModule;
+import org.apache.druid.guice.BloomFilterSerializersModule;
+import org.apache.druid.jackson.DefaultObjectMapper;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.query.aggregation.Aggregator;
+import org.apache.druid.query.aggregation.AggregatorFactory;
+import org.apache.druid.query.aggregation.BufferAggregator;
+import 
org.apache.druid.query.aggregation.cardinality.CardinalityAggregatorTest;
+import org.apache.druid.query.dimension.DefaultDimensionSpec;
+import org.apache.druid.query.dimension.DimensionSpec;
+import org.apache.druid.query.dimension.ExtractionDimensionSpec;
+import org.apache.druid.query.dimension.RegexFilteredDimensionSpec;
+import org.apache.druid.query.extraction.RegexDimExtractionFn;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector;
+import org.apache.druid.segment.ColumnValueSelector;
+import org.apache.druid.segment.DimensionSelector;
+import org.apache.druid.segment.DoubleColumnSelector;
+import org.apache.druid.segment.FloatColumnSelector;
+import org.apache.druid.segment.LongColumnSelector;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class BloomFilterAggregatorTest
+{
+  private static final String nullish = NullHandling.replaceWithDefault() ? "" 
: null;
+  private static final List<String[]> values1 = dimensionValues(
+      "a",
+      "b",
+      "c",
+      "a",
+      "a",
+      nullish,
+      "b",
+      "b",
+      "b",
+      "b",
+      "a",
+      "a"
+  );
+  private static final List<String[]> values2 = dimensionValues(
+      "a",
+      "b",
+      "c",
+      "x",
+      "a",
+      "e",
+      "b",
+      new String[]{nullish, "x"},
+      new String[]{"x", nullish},
+      new String[]{"y", "x"},
+      new String[]{"x", "y"},
+      new String[]{"x", "y", "a"}
+  );
+  private static final Double[] doubleValues1 = new Double[]{0.1, 1.5, 18.3, 
0.1};
+  private static final Float[] floatValues1 = new Float[]{0.4f, 0.8f, 23.2f};
+  private static final Long[] longValues1 = new Long[]{10241L, 12312355L, 0L, 
81L};
+
+  private static final int maxNumValues = 15;
+
+  private static BloomKFilter filter1;
+  private static BloomKFilter filter2;
+
+  private static String serializedFilter1;
+  private static String serializedFilter2;
+  private static String serializedCombinedFilter;
+  private static String serializedLongFilter;
+  private static String serializedDoubleFilter;
+  private static String serializedFloatFilter;
+
+  static {
+    try {
+      filter1 = new BloomKFilter(maxNumValues);
+      filter2 = new BloomKFilter(maxNumValues);
+      BloomKFilter combinedValuesFilter = new BloomKFilter(maxNumValues);
+
+      createStringFilter(values1, filter1, combinedValuesFilter);
+      createStringFilter(values2, filter2, combinedValuesFilter);
+
+      serializedFilter1 = filterToString(filter1);
+      serializedFilter2 = filterToString(filter2);
+      serializedCombinedFilter = filterToString(combinedValuesFilter);
+
+      BloomKFilter longFilter = new BloomKFilter(maxNumValues);
+      for (long val : longValues1) {
+        longFilter.addLong(val);
+      }
+      serializedLongFilter = filterToString(longFilter);
+
+      BloomKFilter floatFilter = new BloomKFilter(maxNumValues);
+      for (float val : floatValues1) {
+        floatFilter.addFloat(val);
+      }
+      serializedFloatFilter = filterToString(floatFilter);
+
+      BloomKFilter doubleFilter = new BloomKFilter(maxNumValues);
+      for (double val : doubleValues1) {
+        doubleFilter.addDouble(val);
+      }
+      serializedDoubleFilter = filterToString(doubleFilter);
+
+    }
+    catch (Exception ex) {
+      throw new RuntimeException(ex);
+    }
+  }
+
+  private static void createStringFilter(List<String[]> values, BloomKFilter 
filter, BloomKFilter combinedValuesFilter)
+  {
+    for (String[] vals : values) {
+      for (String val : vals) {
+        if (!NullHandling.replaceWithDefault() && val == null) {
+          filter.addBytes(null, 0, 0);
+          combinedValuesFilter.addBytes(null, 0, 0);
+        } else {
+          filter.addString(NullHandling.nullToEmptyIfNeeded(val));
+          
combinedValuesFilter.addString(NullHandling.nullToEmptyIfNeeded(val));
+        }
+      }
+    }
+  }
+
+  private static List<String[]> dimensionValues(Object... values)
+  {
+    return Lists.transform(
+        Lists.newArrayList(values), new Function<Object, String[]>()
+        {
+          @Nullable
+          @Override
+          public String[] apply(@Nullable Object input)
+          {
+            if (input instanceof String[]) {
+              return (String[]) input;
+            } else {
+              return new String[]{(String) input};
+            }
+          }
+        }
+    );
+  }
+
+  private static void aggregateDimension(List<DimensionSelector> selectorList, 
Aggregator agg)
+  {
+    agg.aggregate();
+
+    for (DimensionSelector selector : selectorList) {
+      ((CardinalityAggregatorTest.TestDimensionSelector) selector).increment();
+    }
+  }
+
+  private static void bufferAggregateDimension(
+      List<DimensionSelector> selectorList,
+      BufferAggregator agg,
+      ByteBuffer buf,
+      int pos
+  )
+  {
+    agg.aggregate(buf, pos);
+
+    for (DimensionSelector selector : selectorList) {
+      ((CardinalityAggregatorTest.TestDimensionSelector) selector).increment();
+    }
+  }
+
+  private static void aggregateColumn(List<SteppableSelector> selectorList, 
Aggregator agg)
+  {
+    agg.aggregate();
+
+    for (SteppableSelector selector : selectorList) {
+      selector.increment();
+    }
+  }
+
+  private static void bufferAggregateColumn(
+      List<SteppableSelector> selectorList,
+      BufferAggregator agg,
+      ByteBuffer buf,
+      int pos
+  )
+  {
+    agg.aggregate(buf, pos);
+
+    for (SteppableSelector selector : selectorList) {
+      selector.increment();
+    }
+  }
+
+  static String filterToString(BloomKFilter bloomKFilter) throws IOException
+  {
+    return 
StringUtils.encodeBase64String(BloomFilterSerializersModule.bloomKFilterToBytes(bloomKFilter));
+  }
+
+  private final DimensionSpec dimSpec = new DefaultDimensionSpec("dim1", 
"dim1");
+  private BloomFilterAggregatorFactory valueAggregatorFactory;
+
+  public BloomFilterAggregatorTest()
+  {
+    valueAggregatorFactory = new BloomFilterAggregatorFactory(
+        "billy",
+        dimSpec,
+        maxNumValues
+    );
+  }
+
+
+  @Test
+  public void testAggregateValues() throws IOException
+  {
+    DimensionSelector dimSelector = new 
CardinalityAggregatorTest.TestDimensionSelector(values1, null);
+    StringBloomFilterAggregator agg = new 
StringBloomFilterAggregator(dimSelector, new BloomKFilter(maxNumValues));
+
+    for (int i = 0; i < values1.size(); ++i) {
+      aggregateDimension(Collections.singletonList(dimSelector), agg);
+    }
+
+    BloomKFilter bloomKFilter = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(agg.get());
+    String serialized = filterToString(bloomKFilter);
+    Assert.assertEquals(serializedFilter1, serialized);
+  }
+
+  @Test
+  public void testAggregateLongValues() throws IOException
+  {
+    TestLongColumnSelector selector = new 
TestLongColumnSelector(Arrays.asList(longValues1));
+    LongBloomFilterAggregator agg = new LongBloomFilterAggregator(selector, 
new BloomKFilter(maxNumValues));
+
+    for (Long ignored : longValues1) {
+      aggregateColumn(Collections.singletonList(selector), agg);
+    }
+
+    BloomKFilter bloomKFilter = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(agg.get());
+    String serialized = filterToString(bloomKFilter);
+    Assert.assertEquals(serializedLongFilter, serialized);
+  }
+
+  @Test
+  public void testAggregateFloatValues() throws IOException
+  {
+    TestFloatColumnSelector selector = new 
TestFloatColumnSelector(Arrays.asList(floatValues1));
+    FloatBloomFilterAggregator agg = new FloatBloomFilterAggregator(selector, 
new BloomKFilter(maxNumValues));
+
+    for (Float ignored : floatValues1) {
+      aggregateColumn(Collections.singletonList(selector), agg);
+    }
+
+    BloomKFilter bloomKFilter = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(agg.get());
+    String serialized = filterToString(bloomKFilter);
+    Assert.assertEquals(serializedFloatFilter, serialized);
+  }
+
+  @Test
+  public void testAggregateDoubleValues() throws IOException
+  {
+    TestDoubleColumnSelector selector = new 
TestDoubleColumnSelector(Arrays.asList(doubleValues1));
+    DoubleBloomFilterAggregator agg = new 
DoubleBloomFilterAggregator(selector, new BloomKFilter(maxNumValues));
+
+    for (Double ignored : doubleValues1) {
+      aggregateColumn(Collections.singletonList(selector), agg);
+    }
+
+    BloomKFilter bloomKFilter = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(agg.get());
+    String serialized = filterToString(bloomKFilter);
+    Assert.assertEquals(serializedDoubleFilter, serialized);
+  }
+
+  @Test
+  public void testBufferAggregateStringValues() throws IOException
+  {
+    DimensionSelector dimSelector = new 
CardinalityAggregatorTest.TestDimensionSelector(values2, null);
+    StringBloomFilterBufferAggregator agg = new 
StringBloomFilterBufferAggregator(dimSelector, maxNumValues);
+
+    int maxSize = valueAggregatorFactory.getMaxIntermediateSizeWithNulls();
+    ByteBuffer buf = ByteBuffer.allocate(maxSize + 64);
+    int pos = 10;
+    buf.limit(pos + maxSize);
+
+    agg.init(buf, pos);
+
+    for (int i = 0; i < values2.size(); ++i) {
+      bufferAggregateDimension(Collections.singletonList(dimSelector), agg, 
buf, pos);
+    }
+    BloomKFilter bloomKFilter = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(agg.get(buf, pos));
+    String serialized = filterToString(bloomKFilter);
+    Assert.assertEquals(serializedFilter2, serialized);
+  }
+
+  @Test
+  public void testBufferAggregateLongValues() throws IOException
+  {
+    TestLongColumnSelector selector = new 
TestLongColumnSelector(Arrays.asList(longValues1));
+    LongBloomFilterBufferAggregator agg = new 
LongBloomFilterBufferAggregator(selector, maxNumValues);
+
+    int maxSize = valueAggregatorFactory.getMaxIntermediateSizeWithNulls();
+    ByteBuffer buf = ByteBuffer.allocate(maxSize + 64);
+    int pos = 10;
+    buf.limit(pos + maxSize);
+
+    agg.init(buf, pos);
+
+    IntStream.range(0, longValues1.length)
+             .forEach(i -> 
bufferAggregateColumn(Collections.singletonList(selector), agg, buf, pos));
+    BloomKFilter bloomKFilter = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(agg.get(buf, pos));
+    String serialized = filterToString(bloomKFilter);
+    Assert.assertEquals(serializedLongFilter, serialized);
+  }
+
+  @Test
+  public void testBufferAggregateFloatValues() throws IOException
+  {
+    TestFloatColumnSelector selector = new 
TestFloatColumnSelector(Arrays.asList(floatValues1));
+    FloatBloomFilterBufferAggregator agg = new 
FloatBloomFilterBufferAggregator(selector, maxNumValues);
+
+    int maxSize = valueAggregatorFactory.getMaxIntermediateSizeWithNulls();
+    ByteBuffer buf = ByteBuffer.allocate(maxSize + 64);
+    int pos = 10;
+    buf.limit(pos + maxSize);
+
+    agg.init(buf, pos);
+
+    IntStream.range(0, floatValues1.length)
+             .forEach(i -> 
bufferAggregateColumn(Collections.singletonList(selector), agg, buf, pos));
+    BloomKFilter bloomKFilter = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(agg.get(buf, pos));
+    String serialized = filterToString(bloomKFilter);
+    Assert.assertEquals(serializedFloatFilter, serialized);
+  }
+
+  @Test
+  public void testBufferAggregateDoubleValues() throws IOException
+  {
+    TestDoubleColumnSelector selector = new 
TestDoubleColumnSelector(Arrays.asList(doubleValues1));
+    DoubleBloomFilterBufferAggregator agg = new 
DoubleBloomFilterBufferAggregator(selector, maxNumValues);
+
+    int maxSize = valueAggregatorFactory.getMaxIntermediateSizeWithNulls();
+    ByteBuffer buf = ByteBuffer.allocate(maxSize + 64);
+    int pos = 10;
+    buf.limit(pos + maxSize);
+
+    agg.init(buf, pos);
+
+    IntStream.range(0, doubleValues1.length)
+             .forEach(i -> 
bufferAggregateColumn(Collections.singletonList(selector), agg, buf, pos));
+    BloomKFilter bloomKFilter = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(agg.get(buf, pos));
+    String serialized = filterToString(bloomKFilter);
+    Assert.assertEquals(serializedDoubleFilter, serialized);
+  }
+
+  @Test
+  public void testCombineValues() throws IOException
+  {
+    DimensionSelector dimSelector1 = new 
CardinalityAggregatorTest.TestDimensionSelector(values1, null);
+    DimensionSelector dimSelector2 = new 
CardinalityAggregatorTest.TestDimensionSelector(values2, null);
+
+    StringBloomFilterAggregator agg1 = new 
StringBloomFilterAggregator(dimSelector1, new BloomKFilter(maxNumValues));
+    StringBloomFilterAggregator agg2 = new 
StringBloomFilterAggregator(dimSelector2, new BloomKFilter(maxNumValues));
+
+    for (int i = 0; i < values1.size(); ++i) {
+      aggregateDimension(Collections.singletonList(dimSelector1), agg1);
+    }
+    for (int i = 0; i < values2.size(); ++i) {
+      aggregateDimension(Collections.singletonList(dimSelector2), agg2);
+    }
+
+    BloomKFilter combined = (BloomKFilter) 
valueAggregatorFactory.finalizeComputation(
+        valueAggregatorFactory.combine(
+            agg1.get(),
+            agg2.get()
+        )
+    );
+
+    String serialized = filterToString(combined);
+    Assert.assertEquals(serializedCombinedFilter, serialized);
+  }
+
+  @Test
+  public void testMergeValues() throws IOException
+  {
+    final TestBloomFilterColumnSelector mergeDim =
+        new TestBloomFilterColumnSelector(ImmutableList.of(filter1, filter2));
+
+    BloomFilterMergeAggregator mergeAggregator =
+        new BloomFilterMergeAggregator(mergeDim, new 
BloomKFilter(maxNumValues));
+
+    for (int i = 0; i < 2; ++i) {
+      aggregateColumn(Collections.singletonList(mergeDim), mergeAggregator);
+    }
+
+
+    BloomKFilter merged = (BloomKFilter) 
valueAggregatorFactory.getCombiningFactory()
+                                                               
.finalizeComputation(mergeAggregator.get());
+    String serialized = filterToString(merged);
+    Assert.assertEquals(serializedCombinedFilter, serialized);
+  }
+
+  @Test
+  public void testMergeValuesWithBuffersForGroupByV1() throws IOException
+  {
+    final TestBloomFilterColumnSelector mergeDim =
+        new TestBloomFilterColumnSelector(
+            ImmutableList.of(
+                
ByteBuffer.wrap(BloomFilterSerializersModule.bloomKFilterToBytes(filter1)),
+                
ByteBuffer.wrap(BloomFilterSerializersModule.bloomKFilterToBytes(filter2))
+            )
+        );
+
+    BloomFilterMergeAggregator mergeAggregator =
+        new BloomFilterMergeAggregator(mergeDim, new 
BloomKFilter(maxNumValues));
+
+    for (int i = 0; i < 2; ++i) {
+      aggregateColumn(Collections.singletonList(mergeDim), mergeAggregator);
+    }
+
+
+    BloomKFilter merged = (BloomKFilter) 
valueAggregatorFactory.getCombiningFactory()
+                                                               
.finalizeComputation(mergeAggregator.get());
+    String serialized = filterToString(merged);
+    Assert.assertEquals(serializedCombinedFilter, serialized);
+  }
+
+  @Test
+  public void testBuferMergeValues() throws IOException
+  {
+    final TestBloomFilterBufferColumnSelector mergeDim =
+        new TestBloomFilterBufferColumnSelector(
+            ImmutableList.of(
+                
ByteBuffer.wrap(BloomFilterSerializersModule.bloomKFilterToBytes(filter1)),
+                
ByteBuffer.wrap(BloomFilterSerializersModule.bloomKFilterToBytes(filter2))
+            )
+        );
+
+    BloomFilterMergeBufferAggregator mergeAggregator = new 
BloomFilterMergeBufferAggregator(mergeDim, maxNumValues);
+
+    int maxSize = 
valueAggregatorFactory.getCombiningFactory().getMaxIntermediateSizeWithNulls();
+    ByteBuffer buf = ByteBuffer.allocate(maxSize + 64);
+    int pos = 10;
+    buf.limit(pos + maxSize);
+
+    mergeAggregator.init(buf, pos);
+
+    for (int i = 0; i < 2; ++i) {
+      bufferAggregateColumn(Collections.singletonList(mergeDim), 
mergeAggregator, buf, pos);
+    }
+
+    BloomKFilter merged = (BloomKFilter) 
valueAggregatorFactory.getCombiningFactory()
+                                                               
.finalizeComputation(mergeAggregator.get(buf, pos));
+    String serialized = filterToString(merged);
+
+    Assert.assertEquals(serializedCombinedFilter, serialized);
+  }
+
+  @Test
+  public void testSerde() throws Exception
+  {
+    BloomFilterAggregatorFactory factory = new BloomFilterAggregatorFactory(
+        "billy",
+        new DefaultDimensionSpec("b", "b"),
+        maxNumValues
+    );
+    ObjectMapper objectMapper = new DefaultObjectMapper();
+    new 
BloomFilterExtensionModule().getJacksonModules().forEach(objectMapper::registerModule);
+    Assert.assertEquals(
+        factory,
+        objectMapper.readValue(objectMapper.writeValueAsString(factory), 
AggregatorFactory.class)
+    );
+
+    String fieldNamesOnly = "{"
+                            + "\"type\":\"bloom\","
+                            + "\"name\":\"billy\","
+                            + "\"field\":\"b\","
+                            + "\"maxNumEntries\":15"
+                            + "}";
+    Assert.assertEquals(
+        factory,
+        objectMapper.readValue(fieldNamesOnly, AggregatorFactory.class)
+    );
+
+    BloomFilterAggregatorFactory factory2 = new BloomFilterAggregatorFactory(
+        "billy",
+        new ExtractionDimensionSpec("b", "b", new RegexDimExtractionFn(".*", 
false, null)),
+        maxNumValues
+    );
+
+    Assert.assertEquals(
+        factory2,
+        objectMapper.readValue(objectMapper.writeValueAsString(factory2), 
AggregatorFactory.class)
+    );
+
+    BloomFilterAggregatorFactory factory3 = new BloomFilterAggregatorFactory(
+        "billy",
+        new RegexFilteredDimensionSpec(new DefaultDimensionSpec("a", "a"), 
".*"),
+        maxNumValues
+    );
+    Assert.assertEquals(
+        factory3,
+        objectMapper.readValue(objectMapper.writeValueAsString(factory3), 
AggregatorFactory.class)
+    );
+  }
+
+  private abstract static class SteppableSelector<T> implements 
ColumnValueSelector<T>
+  {
+    List<T> values;
+    int pos;
+
+    public SteppableSelector(List<T> values)
+    {
+      this.values = values;
+      this.pos = 0;
+    }
+
+    @Nullable
+    @Override
+    public T getObject()
+    {
+      return values.get(pos);
+    }
+
+    public void increment()
+    {
+      pos++;
+    }
+
+    public void reset()
+    {
+      pos = 0;
+    }
+
+
+    @Override
+    public double getDouble()
+    {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public float getFloat()
+    {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getLong()
+    {
+      throw new UnsupportedOperationException();
+    }
+
+
+    @Override
+    public void inspectRuntimeShape(RuntimeShapeInspector inspector)
+    {
+
+    }
+
+    @Override
+    public Class<T> classOfObject()
+    {
+      return null;
+    }
+
+    @Override
+    public boolean isNull()
+    {
+      return false;
+    }
+  }
+
+  public static class TestBloomFilterColumnSelector extends 
SteppableSelector<Object>
+  {
+    public TestBloomFilterColumnSelector(List<Object> values)
+    {
+      super(values);
+    }
+  }
+
+  public static class TestBloomFilterBufferColumnSelector extends 
SteppableSelector<ByteBuffer>
+  {
+    public TestBloomFilterBufferColumnSelector(List<ByteBuffer> values)
+    {
+      super(values);
+    }
+  }
+
+  public static class TestLongColumnSelector extends SteppableSelector<Long> 
implements LongColumnSelector
+  {
+    public TestLongColumnSelector(List<Long> values)
+    {
+      super(values);
+    }
+
+    @Override
+    public long getLong()
+    {
+      return values.get(pos);
+    }
+  }
+
+  public static class TestFloatColumnSelector extends SteppableSelector<Float> 
implements FloatColumnSelector
+  {
+    public TestFloatColumnSelector(List<Float> values)
+    {
+      super(values);
+    }
+
+    @Override
+    public float getFloat()
+    {
+      return values.get(pos);
+    }
+  }
+
+  public static class TestDoubleColumnSelector extends 
SteppableSelector<Double> implements DoubleColumnSelector
+  {
+    public TestDoubleColumnSelector(List<Double> values)
+    {
+      super(values);
+    }
+
+    @Override
+    public double getDouble()
+    {
+      return values.get(pos);
+    }
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/aggregation/bloom/BloomFilterGroupByQueryTest.java
 
b/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/aggregation/bloom/BloomFilterGroupByQueryTest.java
new file mode 100644
index 0000000..a2207f2
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/aggregation/bloom/BloomFilterGroupByQueryTest.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.aggregation.bloom;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.inject.Guice;
+import com.google.inject.Key;
+import org.apache.druid.data.input.MapBasedRow;
+import org.apache.druid.guice.BloomFilterExtensionModule;
+import org.apache.druid.guice.annotations.Json;
+import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.java.util.common.guava.Sequence;
+import org.apache.druid.query.aggregation.AggregationTestHelper;
+import org.apache.druid.query.filter.BloomKFilter;
+import org.apache.druid.query.groupby.GroupByQueryConfig;
+import org.apache.druid.query.groupby.GroupByQueryRunnerTest;
+import org.apache.druid.segment.TestHelper;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public class BloomFilterGroupByQueryTest
+{
+  private static final BloomFilterExtensionModule module = new 
BloomFilterExtensionModule();
+
+  static {
+    // throwaway, just using to properly initialize jackson modules
+    Guice.createInjector(
+        binder -> binder.bind(Key.get(ObjectMapper.class, 
Json.class)).toInstance(TestHelper.makeJsonMapper()),
+        module
+    );
+  }
+
+  private AggregationTestHelper helper;
+
+  @Rule
+  public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+  public BloomFilterGroupByQueryTest(final GroupByQueryConfig config)
+  {
+    helper = AggregationTestHelper.createGroupByQueryAggregationTestHelper(
+        Lists.newArrayList(module.getJacksonModules()),
+        config,
+        tempFolder
+    );
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static Collection<?> constructorFeeder()
+  {
+    final List<Object[]> constructors = new ArrayList<>();
+    for (GroupByQueryConfig config : GroupByQueryRunnerTest.testConfigs()) {
+      constructors.add(new Object[]{config});
+    }
+    return constructors;
+  }
+
+  @After
+  public void teardown() throws IOException
+  {
+    helper.close();
+  }
+
+  @Test
+  public void testQuery() throws Exception
+  {
+
+    String query = "{"
+                   + "\"queryType\": \"groupBy\","
+                   + "\"dataSource\": \"test_datasource\","
+                   + "\"granularity\": \"ALL\","
+                   + "\"dimensions\": [],"
+                   + "\"filter\":{ \"type\":\"selector\", 
\"dimension\":\"market\", \"value\":\"upfront\"},"
+                   + "\"aggregations\": ["
+                   + "  { \"type\": \"bloom\", \"name\": \"blooming_quality\", 
\"field\": \"quality\" }"
+                   + "],"
+                   + "\"intervals\": [ \"1970/2050\" ]"
+                   + "}";
+
+    MapBasedRow row = ingestAndQuery(query);
+
+
+    Assert.assertTrue(((BloomKFilter) 
row.getRaw("blooming_quality")).testString("mezzanine"));
+    Assert.assertTrue(((BloomKFilter) 
row.getRaw("blooming_quality")).testString("premium"));
+    Assert.assertFalse(((BloomKFilter) 
row.getRaw("blooming_quality")).testString("entertainment"));
+  }
+
+  @Test
+  public void testQueryFakeDimension() throws Exception
+  {
+    String query = "{"
+                   + "\"queryType\": \"groupBy\","
+                   + "\"dataSource\": \"test_datasource\","
+                   + "\"granularity\": \"ALL\","
+                   + "\"dimensions\": [],"
+                   + "\"filter\":{ \"type\":\"selector\", 
\"dimension\":\"market\", \"value\":\"upfront\"},"
+                   + "\"aggregations\": ["
+                   + "  { \"type\": \"bloom\", \"name\": \"blooming_quality\", 
\"field\": \"nope\" }"
+                   + "],"
+                   + "\"intervals\": [ \"1970/2050\" ]"
+                   + "}";
+
+    MapBasedRow row = ingestAndQuery(query);
+
+    // a nil column results in a totally empty bloom filter
+    BloomKFilter filter = new BloomKFilter(1500);
+
+    Object val = row.getRaw("blooming_quality");
+
+    String serialized = 
BloomFilterAggregatorTest.filterToString((BloomKFilter) val);
+    String empty = BloomFilterAggregatorTest.filterToString(filter);
+
+    Assert.assertEquals(empty, serialized);
+  }
+
+  private MapBasedRow ingestAndQuery(String query) throws Exception
+  {
+    String metricSpec = "[{ \"type\": \"count\", \"name\": \"count\"}]";
+
+    String parseSpec = "{"
+                       + "\"type\" : \"string\","
+                       + "\"parseSpec\" : {"
+                       + "    \"format\" : \"tsv\","
+                       + "    \"timestampSpec\" : {"
+                       + "        \"column\" : \"timestamp\","
+                       + "        \"format\" : \"auto\""
+                       + "},"
+                       + "    \"dimensionsSpec\" : {"
+                       + "        \"dimensions\": [],"
+                       + "        \"dimensionExclusions\" : [],"
+                       + "        \"spatialDimensions\" : []"
+                       + "    },"
+                       + "    \"columns\": [\"timestamp\", \"market\", 
\"quality\", \"placement\", \"placementish\", \"index\"]"
+                       + "  }"
+                       + "}";
+
+    Sequence seq = helper.createIndexAndRunQueryOnSegment(
+        
this.getClass().getClassLoader().getResourceAsStream("sample.data.tsv"),
+        parseSpec,
+        metricSpec,
+        0,
+        Granularities.NONE,
+        50000,
+        query
+    );
+
+    return (MapBasedRow) seq.toList().get(0);
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/filter/BloomKFilterTest.java
 
b/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/filter/BloomKFilterTest.java
new file mode 100644
index 0000000..3385924
--- /dev/null
+++ 
b/extensions-core/druid-bloom-filter/src/test/java/org/apache/druid/query/filter/BloomKFilterTest.java
@@ -0,0 +1,541 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.query.filter;
+
+import org.apache.druid.io.ByteBufferInputStream;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static org.junit.Assert.assertEquals;
+
+public class BloomKFilterTest
+{
+  private static final int COUNT = 100;
+  Random rand = ThreadLocalRandom.current();
+
+  @Test
+  public void testBloomKFilterBytes() throws IOException
+  {
+    BloomKFilter bf = new BloomKFilter(10000);
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bf);
+    byte[] bfBytes = bytesOut.toByteArray();
+    ByteBuffer buffer = ByteBuffer.wrap(bfBytes);
+
+    byte[] val = new byte[]{1, 2, 3};
+    byte[] val1 = new byte[]{1, 2, 3, 4};
+    byte[] val2 = new byte[]{1, 2, 3, 4, 5};
+    byte[] val3 = new byte[]{1, 2, 3, 4, 5, 6};
+
+
+    bf.add(val);
+    BloomKFilter.add(buffer, val);
+    BloomKFilter rehydrated = BloomKFilter.deserialize(new 
ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.test(val));
+    assertEquals(false, rehydrated.test(val1));
+    assertEquals(false, rehydrated.test(val2));
+    assertEquals(false, rehydrated.test(val3));
+    BloomKFilter.add(buffer, val1);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.test(val));
+    assertEquals(true, rehydrated.test(val1));
+    assertEquals(false, rehydrated.test(val2));
+    assertEquals(false, rehydrated.test(val3));
+    BloomKFilter.add(buffer, val2);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.test(val));
+    assertEquals(true, rehydrated.test(val1));
+    assertEquals(true, rehydrated.test(val2));
+    assertEquals(false, rehydrated.test(val3));
+    BloomKFilter.add(buffer, val3);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.test(val));
+    assertEquals(true, rehydrated.test(val1));
+    assertEquals(true, rehydrated.test(val2));
+    assertEquals(true, rehydrated.test(val3));
+
+    byte[] randVal = new byte[COUNT];
+    for (int i = 0; i < COUNT; i++) {
+      rand.nextBytes(randVal);
+      BloomKFilter.add(buffer, randVal);
+    }
+    // last value should be present
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    assertEquals(true, rehydrated.test(randVal));
+    // most likely this value should not exist
+    randVal[0] = 0;
+    randVal[1] = 0;
+    randVal[2] = 0;
+    randVal[3] = 0;
+    randVal[4] = 0;
+    assertEquals(false, rehydrated.test(randVal));
+
+    assertEquals(7808, rehydrated.sizeInBytes());
+  }
+
+  @Test
+  public void testBloomKFilterByte() throws IOException
+  {
+    BloomKFilter bf = new BloomKFilter(10000);
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bf);
+    byte[] bfBytes = bytesOut.toByteArray();
+    ByteBuffer buffer = ByteBuffer.wrap(bfBytes);
+
+    byte val = Byte.MIN_VALUE;
+    byte val1 = 1;
+    byte val2 = 2;
+    byte val3 = Byte.MAX_VALUE;
+
+    BloomKFilter.addLong(buffer, val);
+    BloomKFilter rehydrated = BloomKFilter.deserialize(new 
ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(false, rehydrated.testLong(val1));
+    assertEquals(false, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val1);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(false, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val2);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(true, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val3);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(true, rehydrated.testLong(val2));
+    assertEquals(true, rehydrated.testLong(val3));
+
+    byte randVal = 0;
+    for (int i = 0; i < COUNT; i++) {
+      randVal = (byte) rand.nextInt(Byte.MAX_VALUE);
+      BloomKFilter.addLong(buffer, randVal);
+    }
+
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+
+    // last value should be present
+    assertEquals(true, rehydrated.testLong(randVal));
+    // most likely this value should not exist
+    assertEquals(false, rehydrated.testLong((byte) -120));
+
+    assertEquals(7808, rehydrated.sizeInBytes());
+  }
+
+  @Test
+  public void testBloomKFilterInt() throws IOException
+  {
+    BloomKFilter bf = new BloomKFilter(10000);
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bf);
+    byte[] bfBytes = bytesOut.toByteArray();
+    ByteBuffer buffer = ByteBuffer.wrap(bfBytes);
+
+    int val = Integer.MIN_VALUE;
+    int val1 = 1;
+    int val2 = 2;
+    int val3 = Integer.MAX_VALUE;
+
+    BloomKFilter.addLong(buffer, val);
+    BloomKFilter rehydrated = BloomKFilter.deserialize(new 
ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(false, rehydrated.testLong(val1));
+    assertEquals(false, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val1);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(false, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val2);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(true, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val3);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(true, rehydrated.testLong(val2));
+    assertEquals(true, rehydrated.testLong(val3));
+
+    int randVal = 0;
+    for (int i = 0; i < COUNT; i++) {
+      randVal = rand.nextInt();
+      BloomKFilter.addLong(buffer, randVal);
+    }
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    // last value should be present
+    assertEquals(true, rehydrated.testLong(randVal));
+    // most likely this value should not exist
+    assertEquals(false, rehydrated.testLong(-120));
+
+    assertEquals(7808, rehydrated.sizeInBytes());
+  }
+
+  @Test
+  public void testBloomKFilterLong() throws IOException
+  {
+    BloomKFilter bf = new BloomKFilter(10000);
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bf);
+    byte[] bfBytes = bytesOut.toByteArray();
+    ByteBuffer buffer = ByteBuffer.wrap(bfBytes);
+
+    long val = Long.MIN_VALUE;
+    long val1 = 1;
+    long val2 = 2;
+    long val3 = Long.MAX_VALUE;
+
+    BloomKFilter.addLong(buffer, val);
+    BloomKFilter rehydrated = BloomKFilter.deserialize(new 
ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(false, rehydrated.testLong(val1));
+    assertEquals(false, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val1);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(false, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val2);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(true, rehydrated.testLong(val2));
+    assertEquals(false, rehydrated.testLong(val3));
+    BloomKFilter.addLong(buffer, val3);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testLong(val));
+    assertEquals(true, rehydrated.testLong(val1));
+    assertEquals(true, rehydrated.testLong(val2));
+    assertEquals(true, rehydrated.testLong(val3));
+
+    int randVal = 0;
+    for (int i = 0; i < COUNT; i++) {
+      randVal = rand.nextInt();
+      BloomKFilter.addLong(buffer, randVal);
+    }
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    // last value should be present
+    assertEquals(true, rehydrated.testLong(randVal));
+    // most likely this value should not exist
+    assertEquals(false, rehydrated.testLong(-120));
+
+    assertEquals(7808, rehydrated.sizeInBytes());
+  }
+
+  @Test
+  public void testBloomKFilterFloat() throws IOException
+  {
+    BloomKFilter bf = new BloomKFilter(10000);
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bf);
+    byte[] bfBytes = bytesOut.toByteArray();
+    ByteBuffer buffer = ByteBuffer.wrap(bfBytes);
+
+    float val = Float.NEGATIVE_INFINITY;
+    float val1 = 1.1f;
+    float val2 = 2.2f;
+    float val3 = Float.POSITIVE_INFINITY;
+
+    BloomKFilter.addFloat(buffer, val);
+    BloomKFilter rehydrated = BloomKFilter.deserialize(new 
ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testFloat(val));
+    assertEquals(false, rehydrated.testFloat(val1));
+    assertEquals(false, rehydrated.testFloat(val2));
+    assertEquals(false, rehydrated.testFloat(val3));
+    BloomKFilter.addFloat(buffer, val1);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testFloat(val));
+    assertEquals(true, rehydrated.testFloat(val1));
+    assertEquals(false, rehydrated.testFloat(val2));
+    assertEquals(false, rehydrated.testFloat(val3));
+    BloomKFilter.addFloat(buffer, val2);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testFloat(val));
+    assertEquals(true, rehydrated.testFloat(val1));
+    assertEquals(true, rehydrated.testFloat(val2));
+    assertEquals(false, rehydrated.testFloat(val3));
+    BloomKFilter.addFloat(buffer, val3);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testFloat(val));
+    assertEquals(true, rehydrated.testFloat(val1));
+    assertEquals(true, rehydrated.testFloat(val2));
+    assertEquals(true, rehydrated.testFloat(val3));
+
+    float randVal = 0;
+    for (int i = 0; i < COUNT; i++) {
+      randVal = rand.nextFloat();
+      BloomKFilter.addFloat(buffer, randVal);
+    }
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+
+    // last value should be present
+    assertEquals(true, rehydrated.testFloat(randVal));
+    // most likely this value should not exist
+    assertEquals(false, rehydrated.testFloat(-120.2f));
+
+    assertEquals(7808, rehydrated.sizeInBytes());
+  }
+
+  @Test
+  public void testBloomKFilterDouble() throws IOException
+  {
+    BloomKFilter bf = new BloomKFilter(10000);
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bf);
+    byte[] bfBytes = bytesOut.toByteArray();
+    ByteBuffer buffer = ByteBuffer.wrap(bfBytes);
+
+    double val = Double.NEGATIVE_INFINITY;
+    double val1 = 1.1d;
+    double val2 = 2.2d;
+    double val3 = Double.POSITIVE_INFINITY;
+
+    BloomKFilter.addDouble(buffer, val);
+    BloomKFilter rehydrated = BloomKFilter.deserialize(new 
ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testDouble(val));
+    assertEquals(false, rehydrated.testDouble(val1));
+    assertEquals(false, rehydrated.testDouble(val2));
+    assertEquals(false, rehydrated.testDouble(val3));
+    BloomKFilter.addDouble(buffer, val1);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testDouble(val));
+    assertEquals(true, rehydrated.testDouble(val1));
+    assertEquals(false, rehydrated.testDouble(val2));
+    assertEquals(false, rehydrated.testDouble(val3));
+    BloomKFilter.addDouble(buffer, val2);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testDouble(val));
+    assertEquals(true, rehydrated.testDouble(val1));
+    assertEquals(true, rehydrated.testDouble(val2));
+    assertEquals(false, rehydrated.testDouble(val3));
+    BloomKFilter.addDouble(buffer, val3);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testDouble(val));
+    assertEquals(true, rehydrated.testDouble(val1));
+    assertEquals(true, rehydrated.testDouble(val2));
+    assertEquals(true, rehydrated.testDouble(val3));
+
+    double randVal = 0;
+    for (int i = 0; i < COUNT; i++) {
+      randVal = rand.nextDouble();
+      BloomKFilter.addDouble(buffer, randVal);
+    }
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+
+    // last value should be present
+    assertEquals(true, rehydrated.testDouble(randVal));
+    // most likely this value should not exist
+    assertEquals(false, rehydrated.testDouble(-120.2d));
+
+    assertEquals(7808, rehydrated.sizeInBytes());
+  }
+
+  @Test
+  public void testBloomKFilterString() throws IOException
+  {
+    BloomKFilter bf = new BloomKFilter(100000);
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bf);
+    byte[] bfBytes = bytesOut.toByteArray();
+    ByteBuffer buffer = ByteBuffer.wrap(bfBytes);
+
+    String val = "bloo";
+    String val1 = "bloom fil";
+    String val2 = "bloom filter";
+    String val3 = "cuckoo filter";
+
+    BloomKFilter.addString(buffer, val);
+    BloomKFilter rehydrated = BloomKFilter.deserialize(new 
ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testString(val));
+    assertEquals(false, rehydrated.testString(val1));
+    assertEquals(false, rehydrated.testString(val2));
+    assertEquals(false, rehydrated.testString(val3));
+    BloomKFilter.addString(buffer, val1);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testString(val));
+    assertEquals(true, rehydrated.testString(val1));
+    assertEquals(false, rehydrated.testString(val2));
+    assertEquals(false, rehydrated.testString(val3));
+    BloomKFilter.addString(buffer, val2);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testString(val));
+    assertEquals(true, rehydrated.testString(val1));
+    assertEquals(true, rehydrated.testString(val2));
+    assertEquals(false, rehydrated.testString(val3));
+    BloomKFilter.addString(buffer, val3);
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    buffer.position(0);
+    assertEquals(true, rehydrated.testString(val));
+    assertEquals(true, rehydrated.testString(val1));
+    assertEquals(true, rehydrated.testString(val2));
+    assertEquals(true, rehydrated.testString(val3));
+
+    long randVal = 0;
+    for (int i = 0; i < COUNT; i++) {
+      randVal = rand.nextLong();
+      BloomKFilter.addString(buffer, Long.toString(randVal));
+    }
+    rehydrated = BloomKFilter.deserialize(new ByteBufferInputStream(buffer));
+    // last value should be present
+    assertEquals(true, rehydrated.testString(Long.toString(randVal)));
+    // most likely this value should not exist
+    assertEquals(false, rehydrated.testString(Long.toString(-120)));
+
+    assertEquals(77952, rehydrated.sizeInBytes());
+  }
+
+  @Test
+  public void testMergeBloomKFilterByteBuffers() throws Exception
+  {
+    BloomKFilter bf1 = new BloomKFilter(10000);
+    BloomKFilter bf2 = new BloomKFilter(10000);
+
+    String[] inputs1 = {
+        "bloo",
+        "bloom fil",
+        "bloom filter",
+        "cuckoo filter",
+        };
+
+    String[] inputs2 = {
+        "2_bloo",
+        "2_bloom fil",
+        "2_bloom filter",
+        "2_cuckoo filter",
+        };
+
+    for (String val : inputs1) {
+      bf1.addString(val);
+    }
+    for (String val : inputs2) {
+      bf2.addString(val);
+    }
+
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bf1);
+    byte[] bf1Bytes = bytesOut.toByteArray();
+    bytesOut.reset();
+    BloomKFilter.serialize(bytesOut, bf2);
+    byte[] bf2Bytes = bytesOut.toByteArray();
+
+    ByteBuffer buf1 = ByteBuffer.wrap(bf1Bytes);
+    ByteBuffer buf2 = ByteBuffer.wrap(bf2Bytes);
+
+    // Merge bytes
+    BloomKFilter.mergeBloomFilterByteBuffers(
+        buf1,
+        0,
+        buf2,
+        0
+    );
+
+    // Deserialize and test
+    byte[] merged = new byte[bf1Bytes.length];
+    buf1.get(merged, 0, bf1Bytes.length);
+
+    ByteArrayInputStream bytesIn = new ByteArrayInputStream(merged, 0, 
bf1Bytes.length);
+    BloomKFilter bfMerged = BloomKFilter.deserialize(bytesIn);
+    // All values should pass test
+    for (String val : inputs1) {
+      assert bfMerged.testString(val);
+    }
+    for (String val : inputs2) {
+      assert bfMerged.testString(val);
+    }
+  }
+
+  @Test
+  public void testCountBitBloomKFilterByteBuffersEmpty() throws Exception
+  {
+    BloomKFilter bfWithValues = new BloomKFilter(10000);
+    BloomKFilter bfEmpty = new BloomKFilter(10000);
+    BloomKFilter bfNull = new BloomKFilter(10000);
+
+    for (int i = 0; i < 1000; i++) {
+      bfWithValues.addInt(rand.nextInt());
+    }
+
+    bfNull.addBytes(null, 0, 0);
+
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    BloomKFilter.serialize(bytesOut, bfWithValues);
+    ByteBuffer bufWithValues = ByteBuffer.wrap(bytesOut.toByteArray());
+    bytesOut.reset();
+    BloomKFilter.serialize(bytesOut, bfEmpty);
+    ByteBuffer bufEmpty = ByteBuffer.wrap(bytesOut.toByteArray());
+    bytesOut.reset();
+    BloomKFilter.serialize(bytesOut, bfNull);
+    ByteBuffer bufWithNull = ByteBuffer.wrap(bytesOut.toByteArray());
+
+
+    Assert.assertTrue(BloomKFilter.getNumSetBits(bufWithValues, 0) > 0);
+    Assert.assertFalse(BloomKFilter.getNumSetBits(bufEmpty, 0) > 0);
+    Assert.assertTrue(BloomKFilter.getNumSetBits(bufWithNull, 0) > 0);
+    Assert.assertTrue(
+        BloomKFilter.getNumSetBits(bufWithValues, 0) > 
BloomKFilter.getNumSetBits(bufWithNull, 0)
+    );
+  }
+}
diff --git 
a/extensions-core/druid-bloom-filter/src/test/resources/sample.data.tsv 
b/extensions-core/druid-bloom-filter/src/test/resources/sample.data.tsv
new file mode 100644
index 0000000..674d86c
--- /dev/null
+++ b/extensions-core/druid-bloom-filter/src/test/resources/sample.data.tsv
@@ -0,0 +1,13 @@
+2011-04-15T00:00:00.000Z       spot    automotive      preferred       
apreferred     106.793700
+2011-04-15T00:00:00.000Z       spot    business        preferred       
bpreferred     94.469747
+2011-04-15T00:00:00.000Z       spot    entertainment   preferred       
epreferred     135.109191
+2011-04-15T00:00:00.000Z       spot    health  preferred       hpreferred     
99.596909
+2011-04-15T00:00:00.000Z       spot    mezzanine       preferred       
mpreferred     92.782760
+2011-04-15T00:00:00.000Z       spot    news    preferred       npreferred
+2011-04-15T00:00:00.000Z       spot    premium preferred       ppreferred
+2011-04-15T00:00:00.000Z       spot    technology      preferred       
tpreferred
+2011-04-15T00:00:00.000Z       spot    travel  preferred       tpreferred
+2011-04-15T00:00:00.000Z       total_market    mezzanine       preferred       
mpreferred
+2011-04-15T00:00:00.000Z       total_market    premium preferred       
ppreferred
+2011-04-15T00:00:00.000Z       upfront mezzanine       preferred       
mpreferred
+2011-04-15T00:00:00.000Z       upfront premium preferred       ppreferred
diff --git 
a/processing/src/main/java/org/apache/druid/query/aggregation/AggregatorUtil.java
 
b/processing/src/main/java/org/apache/druid/query/aggregation/AggregatorUtil.java
index 32003c6..19cf467 100644
--- 
a/processing/src/main/java/org/apache/druid/query/aggregation/AggregatorUtil.java
+++ 
b/processing/src/main/java/org/apache/druid/query/aggregation/AggregatorUtil.java
@@ -113,6 +113,10 @@ public class AggregatorUtil
   // Fixed buckets histogram aggregator
   public static final byte FIXED_BUCKET_HIST_CACHE_TYPE_ID = 0x33;
 
+  // bloom filter extension
+  public static final byte BLOOM_FILTER_CACHE_TYPE_ID = 0x34;
+  public static final byte BLOOM_FILTER_MERGE_CACHE_TYPE_ID = 0x35;
+
   /**
    * returns the list of dependent postAggregators that should be calculated 
in order to calculate given postAgg
    *
diff --git 
a/processing/src/main/java/org/apache/druid/query/aggregation/cardinality/CardinalityBufferAggregator.java
 
b/processing/src/main/java/org/apache/druid/query/aggregation/cardinality/CardinalityBufferAggregator.java
index abb1577..875cd3d 100644
--- 
a/processing/src/main/java/org/apache/druid/query/aggregation/cardinality/CardinalityBufferAggregator.java
+++ 
b/processing/src/main/java/org/apache/druid/query/aggregation/cardinality/CardinalityBufferAggregator.java
@@ -20,6 +20,7 @@
 package org.apache.druid.query.aggregation.cardinality;
 
 import org.apache.druid.hll.HyperLogLogCollector;
+import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.query.ColumnSelectorPlus;
 import org.apache.druid.query.aggregation.BufferAggregator;
 import 
org.apache.druid.query.aggregation.cardinality.types.CardinalityAggregatorColumnSelectorStrategy;
@@ -112,6 +113,8 @@ public class CardinalityBufferAggregator implements 
BufferAggregator
   @Override
   public void inspectRuntimeShape(RuntimeShapeInspector inspector)
   {
-    inspector.visit("selectorPluses", selectorPluses);
+    for (int i = 0; i < selectorPluses.length; i++) {
+      inspector.visit(StringUtils.format("selector-%d", i), 
selectorPluses[i].getSelector());
+    }
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to