diff --git a/extensions-core/filters/pom.xml b/extensions-core/filters/pom.xml
new file mode 100644
index 000000000000..ad9847b5b9c0
--- /dev/null
+++ b/extensions-core/filters/pom.xml
@@ -0,0 +1,115 @@
+
+
+
+
+ 4.0.0
+
+ io.druid.extensions
+ druid-filters
+ druid-filters
+ Druid filter extensions
+
+
+ io.druid
+ druid
+ 0.9.0-SNAPSHOT
+ ../../pom.xml
+
+
+
+
+ io.druid
+ druid-api
+ ${druid.api.version}
+ provided
+
+
+ io.druid
+ druid-processing
+ ${project.parent.version}
+ provided
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+ provided
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-guava
+ ${jackson.version}
+ provided
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-joda
+ ${jackson.version}
+ provided
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-smile
+ ${jackson.version}
+ provided
+
+
+ com.fasterxml.jackson.jaxrs
+ jackson-jaxrs-json-provider
+ ${jackson.version}
+ provided
+
+
+ com.fasterxml.jackson.jaxrs
+ jackson-jaxrs-smile-provider
+ ${jackson.version}
+ provided
+
+
+
+
+ junit
+ junit
+ test
+
+
+ io.druid
+ druid-processing
+ ${project.parent.version}
+ test-jar
+ test
+
+
+
+
diff --git a/extensions-core/filters/src/main/java/io/druid/query/filter/BinaryOperator.java b/extensions-core/filters/src/main/java/io/druid/query/filter/BinaryOperator.java
new file mode 100644
index 000000000000..d1f204158382
--- /dev/null
+++ b/extensions-core/filters/src/main/java/io/druid/query/filter/BinaryOperator.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.query.filter;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+
+import java.util.Comparator;
+
+/**
+ */
+public enum BinaryOperator
+{
+ GT {
+ @Override
+ public Predicate toPredicate(final Comparator comparator, final String value)
+ {
+ return new Predicate()
+ {
+ @Override
+ public boolean apply(String input)
+ {
+ return comparator.compare(input, value) > 0;
+ }
+ };
+ }
+ },
+ GTE {
+ @Override
+ public Predicate toPredicate(final Comparator comparator, final String value)
+ {
+ return new Predicate()
+ {
+ @Override
+ public boolean apply(String input)
+ {
+ return comparator.compare(input, value) >= 0;
+ }
+ };
+ }
+ },
+ LT {
+ @Override
+ public Predicate toPredicate(final Comparator comparator, final String value)
+ {
+ return new Predicate()
+ {
+ @Override
+ public boolean apply(String input)
+ {
+ return comparator.compare(input, value) < 0;
+ }
+ };
+ }
+ },
+ LTE {
+ @Override
+ public Predicate toPredicate(final Comparator comparator, final String value)
+ {
+ return new Predicate()
+ {
+ @Override
+ public boolean apply(String input)
+ {
+ return comparator.compare(input, value) <= 0;
+ }
+ };
+ }
+ },
+ EQ {
+ @Override
+ public Predicate toPredicate(final Comparator comparator, final String value)
+ {
+ return new Predicate()
+ {
+ @Override
+ public boolean apply(String input)
+ {
+ return comparator.compare(input, value) == 0;
+ }
+ };
+ }
+ },
+ NE {
+ @Override
+ public Predicate toPredicate(final Comparator comparator, final String value)
+ {
+ return new Predicate()
+ {
+ @Override
+ public boolean apply(String input)
+ {
+ return comparator.compare(input, value) != 0;
+ }
+ };
+ }
+ };
+
+ public abstract Predicate toPredicate(Comparator comparator, final String value);
+
+ public static BinaryOperator get(String value)
+ {
+ if (Strings.isNullOrEmpty(value)) {
+ return EQ;
+ }
+ value = value.toLowerCase();
+ switch (value) {
+ case "gt":
+ case "greaterthan":
+ case ">":
+ return GT;
+ case "gte":
+ case "greaterthanorequalto":
+ case ">=":
+ return GTE;
+ case "lt":
+ case "lessthan":
+ case "<":
+ return LT;
+ case "lte":
+ case "lessthanorequalto":
+ case "<=":
+ return LTE;
+ case "eq":
+ case "equals":
+ case "=":
+ case "==":
+ return EQ;
+ case "ne":
+ case "notequals":
+ case "!=":
+ case "<>":
+ return NE;
+ }
+ throw new IllegalArgumentException("Invalid operator " + value);
+ }
+}
diff --git a/extensions-core/filters/src/main/java/io/druid/query/filter/FilterExtensionsModule.java b/extensions-core/filters/src/main/java/io/druid/query/filter/FilterExtensionsModule.java
new file mode 100644
index 000000000000..193d344e98b5
--- /dev/null
+++ b/extensions-core/filters/src/main/java/io/druid/query/filter/FilterExtensionsModule.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.query.filter;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.google.inject.Binder;
+import io.druid.initialization.DruidModule;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class FilterExtensionsModule implements DruidModule
+{
+ public FilterExtensionsModule() {}
+
+ @Override
+ public List extends Module> getJacksonModules()
+ {
+ return Arrays.asList(
+ new SimpleModule("FilterExtensionsModule")
+ .setMixInAnnotation(DimFilter.class, DimFilterMixIn.class)
+ );
+ }
+
+ @Override
+ public void configure(Binder binder)
+ {
+ }
+}
+
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+@JsonSubTypes(value = {
+ @JsonSubTypes.Type(name = "selectorEx", value = SelectorDimFilterExtension.class),
+})
+abstract class DimFilterMixIn
+{
+}
diff --git a/extensions-core/filters/src/main/java/io/druid/query/filter/SelectorDimFilterExtension.java b/extensions-core/filters/src/main/java/io/druid/query/filter/SelectorDimFilterExtension.java
new file mode 100644
index 000000000000..4a842ece01ae
--- /dev/null
+++ b/extensions-core/filters/src/main/java/io/druid/query/filter/SelectorDimFilterExtension.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.query.filter;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import io.druid.query.ordering.StringComparators;
+
+import java.nio.ByteBuffer;
+
+/**
+ */
+public class SelectorDimFilterExtension extends SelectorDimFilter implements DimFilterExtension
+{
+ private final String compareType;
+ private final BinaryOperator operator;
+
+ @JsonCreator
+ public SelectorDimFilterExtension(
+ @JsonProperty("dimension") String dimension,
+ @JsonProperty("value") String value,
+ @JsonProperty("operator") String operator,
+ @JsonProperty("compareType") String compareType
+ )
+ {
+ super(dimension, value);
+ this.operator = BinaryOperator.get(operator);
+ this.compareType = compareType == null ? StringComparators.LEXICOGRAPHIC_NAME : compareType;
+
+ // don't allow null comparison, for now
+ Preconditions.checkArgument(
+ !(this.operator != BinaryOperator.EQ && this.operator != BinaryOperator.NE && Strings.isNullOrEmpty(value)),
+ "null comparison is not allowed, except equals/not-equals"
+ );
+ Preconditions.checkArgument(StringComparators.validate(this.compareType), "Invalid compare type " + compareType);
+ }
+
+ public SelectorDimFilterExtension(String dimension, String value, String operator)
+ {
+ this(dimension, value, operator, null);
+ }
+
+ public SelectorDimFilterExtension(String dimension, String value)
+ {
+ this(dimension, value, null, null);
+ }
+
+ @Override
+ public byte[] getCacheKey()
+ {
+ byte[] cacheKey = super.getCacheKey();
+ byte[] dimensionBytes = com.metamx.common.StringUtils.toUtf8(compareType);
+
+ return ByteBuffer.allocate(cacheKey.length + dimensionBytes.length + 1)
+ .put(cacheKey)
+ .put(dimensionBytes)
+ .put((byte) operator.ordinal())
+ .array();
+ }
+
+ @JsonProperty
+ public String getOperator()
+ {
+ return operator.name();
+ }
+
+ @JsonProperty
+ public String getCompareType()
+ {
+ return compareType;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (super.equals(o)) {
+ SelectorDimFilterExtension that = (SelectorDimFilterExtension) o;
+ return operator.equals(that.operator) && compareType.equals(that.compareType);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = super.hashCode();
+ result = 31 * result + operator.ordinal();
+ result = 31 * result + compareType.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s %s %s %s", dimension, operator.name(), value, compareType);
+ }
+
+ @Override
+ public Filter toFilter()
+ {
+ return new SelectorFilterExtension(dimension, value, compareType, operator);
+ }
+}
diff --git a/extensions-core/filters/src/main/java/io/druid/query/filter/SelectorFilterExtension.java b/extensions-core/filters/src/main/java/io/druid/query/filter/SelectorFilterExtension.java
new file mode 100644
index 000000000000..372e179bc5bc
--- /dev/null
+++ b/extensions-core/filters/src/main/java/io/druid/query/filter/SelectorFilterExtension.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.query.filter;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.metamx.collections.bitmap.BitmapFactory;
+import com.metamx.collections.bitmap.ImmutableBitmap;
+import io.druid.query.dimension.DefaultDimensionSpec;
+import io.druid.query.ordering.StringComparators;
+import io.druid.segment.ColumnSelectorFactory;
+import io.druid.segment.DimensionSelector;
+import io.druid.segment.data.ArrayIndexed;
+import io.druid.segment.data.Indexed;
+import io.druid.segment.data.IndexedInts;
+import io.druid.segment.filter.BooleanValueMatcher;
+
+import java.util.Arrays;
+
+/**
+ */
+public class SelectorFilterExtension implements Filter
+{
+ private final String dimension;
+ private final String value;
+ private final BinaryOperator operator;
+ private final StringComparators.StringComparator comparator;
+
+ public SelectorFilterExtension(
+ String dimension,
+ String value,
+ String compareType,
+ BinaryOperator operator
+ )
+ {
+ this.dimension = dimension;
+ this.value = value;
+ this.comparator = StringComparators.makeComparator(compareType);
+ this.operator = operator;
+ }
+
+ public SelectorFilterExtension(String dimension, String value)
+ {
+ this(dimension, value, StringComparators.LEXICOGRAPHIC_NAME, null);
+ }
+
+ @Override
+ public ImmutableBitmap getBitmapIndex(BitmapIndexSelector selector)
+ {
+ final BitmapFactory factory = selector.getBitmapFactory();
+ final Indexed values = sortValues(selector.getDimensionValues(dimension));
+ switch (operator) {
+ case GT:
+ case GTE: {
+ int index = values.indexOf(value);
+ int start = index < 0 ? -index - 1 : operator == BinaryOperator.GT ? index + 1 : index;
+ ImmutableBitmap bitmap = factory.makeEmptyImmutableBitmap();
+ for (int cursor = start; cursor < values.size(); cursor++) {
+ bitmap = bitmap.union(selector.getBitmapIndex(dimension, values.get(cursor)));
+ }
+ return bitmap;
+ }
+ case LT:
+ case LTE: {
+ int index = values.indexOf(value);
+ int limit = index < 0 ? -index - 2 : operator == BinaryOperator.LT ? index - 1 : index;
+ ImmutableBitmap bitmap = factory.makeEmptyImmutableBitmap();
+ for (int cursor = 0; cursor <= limit; cursor++) {
+ bitmap = bitmap.union(selector.getBitmapIndex(dimension, values.get(cursor)));
+ }
+ return bitmap;
+ }
+ case EQ:
+ return selector.getBitmapIndex(dimension, value);
+ case NE:
+ return factory.complement(selector.getBitmapIndex(dimension, value), selector.getNumRows());
+ }
+ throw new IllegalArgumentException("Not supported operator " + operator);
+ }
+
+ private Indexed sortValues(Indexed values)
+ {
+ if (comparator instanceof StringComparators.LexicographicComparator) {
+ return values;
+ }
+ String[] valueArray = new String[values.size()];
+ for (int i = 0; i < valueArray.length; i++) {
+ valueArray[i] = values.get(i);
+ }
+ Arrays.sort(valueArray, comparator);
+ return new ArrayIndexed<>(valueArray, comparator, String.class);
+ }
+
+ @Override
+ public ValueMatcher makeMatcher(ValueMatcherFactory factory)
+ {
+ if (operator == BinaryOperator.EQ || operator == BinaryOperator.NE) {
+ final ValueMatcher matcher = factory.makeValueMatcher(dimension, value);
+ return operator == BinaryOperator.EQ ? matcher : new RevertedMatcher(matcher);
+ }
+ return factory.makeValueMatcher(dimension, operator.toPredicate(comparator, value));
+ }
+
+ @Override
+ public ValueMatcher makeMatcher(ColumnSelectorFactory columnSelectorFactory)
+ {
+ final DimensionSelector dimensionSelector = columnSelectorFactory.makeDimensionSelector(
+ new DefaultDimensionSpec(dimension, dimension)
+ );
+
+ if (operator == BinaryOperator.EQ || operator == BinaryOperator.NE) {
+ final ValueMatcher matcher = toValueMatcher(dimensionSelector);
+ return operator == BinaryOperator.EQ ? matcher : new RevertedMatcher(matcher);
+ }
+
+ final Predicate predicate = operator.toPredicate(comparator, value);
+ return new ValueMatcher()
+ {
+ @Override
+ public boolean matches()
+ {
+ final IndexedInts row = dimensionSelector.getRow();
+ final int size = row.size();
+ for (int i = 0; i < size; ++i) {
+ if (predicate.apply(dimensionSelector.lookupName(row.get(i)))) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
+ private ValueMatcher toValueMatcher(final DimensionSelector dimensionSelector)
+ {
+ if (dimensionSelector == null) {
+ // Missing columns match a null or empty string value and don't match anything else
+ return new BooleanValueMatcher(Strings.isNullOrEmpty(value));
+ }
+ final int valueId = dimensionSelector.lookupId(value);
+ return new ValueMatcher()
+ {
+ @Override
+ public boolean matches()
+ {
+ final IndexedInts row = dimensionSelector.getRow();
+ final int size = row.size();
+ for (int i = 0; i < size; ++i) {
+ if (row.get(i) == valueId) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
+ // for NE
+ private static class RevertedMatcher implements ValueMatcher
+ {
+ private final ValueMatcher matcher;
+
+ private RevertedMatcher(ValueMatcher matcher) {this.matcher = matcher;}
+
+ @Override
+ public boolean matches()
+ {
+ return !matcher.matches();
+ }
+ }
+}
diff --git a/extensions-core/filters/src/main/resources/META-INF/services/io.druid.initialization.DruidModule b/extensions-core/filters/src/main/resources/META-INF/services/io.druid.initialization.DruidModule
new file mode 100644
index 000000000000..55765444ac7d
--- /dev/null
+++ b/extensions-core/filters/src/main/resources/META-INF/services/io.druid.initialization.DruidModule
@@ -0,0 +1 @@
+io.druid.query.filter.FilterExtensionsModule
diff --git a/extensions-core/filters/src/test/java/io/druid/query/filter/CompareTypeTest.java b/extensions-core/filters/src/test/java/io/druid/query/filter/CompareTypeTest.java
new file mode 100644
index 000000000000..08fdf133af98
--- /dev/null
+++ b/extensions-core/filters/src/test/java/io/druid/query/filter/CompareTypeTest.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.query.filter;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.io.CharSource;
+import com.metamx.common.guava.Sequences;
+import io.druid.jackson.DefaultObjectMapper;
+import io.druid.query.Druids;
+import io.druid.query.QueryRunner;
+import io.druid.query.QueryRunnerTestHelper;
+import io.druid.query.Result;
+import io.druid.query.select.EventHolder;
+import io.druid.query.select.PagingSpec;
+import io.druid.query.select.SelectQuery;
+import io.druid.query.select.SelectQueryEngine;
+import io.druid.query.select.SelectQueryQueryToolChest;
+import io.druid.query.select.SelectQueryRunnerFactory;
+import io.druid.query.select.SelectResultValue;
+import io.druid.segment.IncrementalIndexSegment;
+import io.druid.segment.QueryableIndex;
+import io.druid.segment.QueryableIndexSegment;
+import io.druid.segment.TestIndex;
+import io.druid.segment.incremental.IncrementalIndex;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import static io.druid.query.QueryRunnerTestHelper.makeQueryRunner;
+import static io.druid.query.QueryRunnerTestHelper.marketDimension;
+import static io.druid.query.ordering.StringComparators.ALPHANUMERIC_NAME;
+import static io.druid.query.ordering.StringComparators.LEXICOGRAPHIC_NAME;
+import static io.druid.query.ordering.StringComparators.NUMERIC_NAME;
+
+/**
+ */
+@RunWith(Parameterized.class)
+public class CompareTypeTest
+{
+ @Parameterized.Parameters
+ public static Iterable