apache/druid · error · RE
Unable to compare unexpected types
Error message
Unable to compare unexpected types [%s]
What it means
The static COMPARATOR in BloomFilterAggregatorFactory can only compare pairs of ByteBuffer or pairs of BloomKFilter objects, ranking them by number of set bits. If it is handed any other type (or a mixed pair), it throws this RuntimeException with the actual class name of o1. This guards the query machinery against feeding unsupported intermediate objects into sorting/merging of bloom filter aggregation results.
Solutions
- Pass the object through the aggregator factory's deserialize(Object) method (base64 String -> ByteBuffer) before comparing
- Ensure only ByteBuffer (post-deserialize) or BloomKFilter objects are given to this comparator; convert byte[] with ByteBuffer.wrap
- Check all Druid cluster nodes run the same druid-bloom-filter extension version
- If using the comparator directly, instanceof-check inputs first and reject/convert unknown types
Example fix
// before
Object a = "<base64 string from JSON result>";
int cmp = factory.getComparator().compare(a, b); // throws RE
// after
Object a = factory.deserialize("<base64 string from JSON result>"); // -> ByteBuffer
int cmp = factory.getComparator().compare(a, factory.deserialize(b)); Defensive patterns
Strategy: type-guard
Validate before calling
// Java: ensure inputs are comparable before invoking COMPARATOR
static boolean comparable(Object o1, Object o2) {
boolean b1 = o1 instanceof ByteBuffer || o1 instanceof BloomKFilter;
boolean b2 = o2 instanceof ByteBuffer || o2 instanceof BloomKFilter;
return b1 && b2 && o1.getClass().equals(o2.getClass());
} Type guard
if (!(o instanceof ByteBuffer) && !(o instanceof BloomKFilter)) { o = factory.deserialize(o); } Try / catch
try {
comparator.compare(a, b);
} catch (RuntimeException e) {
if (e.getMessage().startsWith("Unable to compare unexpected types")) {
a = factory.deserialize(a); b = factory.deserialize(b); // retry after conversion
} else throw e;
} Prevention
- Always route result objects through BloomFilterAggregatorFactory.deserialize() before sorting/comparing
- Never compare raw JSON (base64 String) results with this comparator
- Keep extension versions consistent across all Druid nodes
When it happens
Trigger: Calling getComparator()-based comparisons (e.g. in merge/sort buffers or GroupByMerge query machinery) with objects that are neither ByteBuffer nor BloomKFilter, or comparing one of each; typically caused by a corrupted intermediate result, a custom aggregator subclass returning the wrong type, or deserialized results not passing through BloomFilterAggregatorFactory.deserialize().
Common situations: Distributed/group-by queries where intermediate results were serialized differently than expected; custom query tooling invoking the comparator directly with byte[] or String results (the native JSON result is base64 String, which deserialize() must convert to ByteBuffer first); version skew between nodes producing different intermediate representations.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- attempt to get boolean[] null vector from string[] only…
- attempt to get double[] from string[] only scalar binding
- attempt to get long[] from string[] only scalar binding
- bf1Length does not match bf2Length
- Bloom filter aggregators are query-time only
AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07).
Data as JSON: /api/errors/d8fcbe803d87a373.
Report an issue: GitHub.
Appendix: source
Thrown at extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorFactory.java:71
private static final int DEFAULT_NUM_ENTRIES = 1500;
public 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 f1 = (BloomKFilter) o1;
BloomKFilter f2 = (BloomKFilter) o2;
return Integer.compare(
f1.getNumSetBits(),
f2.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,
@JsonProperty("maxNumEntries") @Nullable Integer maxNumEntries
)
{
this.name = name;
this.field = field;
this.maxNumEntries = maxNumEntries != null ? maxNumEntries : DEFAULT_NUM_ENTRIES;
}View on GitHub (pinned to 9b90983fd2)