Skip to content

Commit e1277d3

Browse files
dylwyliedrcrallen
authored andcommitted
Use a bimap for reverse lookups on injective maps (apache#5681)
* Use a bimap for reverse lookups on injective maps - A BiMap provides constant-time lookups for mapping values to keys * Address comments * Fix Tests
1 parent 67d0b0e commit e1277d3

File tree

3 files changed

+40
-11
lines changed

3 files changed

+40
-11
lines changed

processing/src/main/java/io/druid/query/extraction/MapLookupExtractor.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
import com.fasterxml.jackson.annotation.JsonProperty;
2424
import com.fasterxml.jackson.annotation.JsonTypeName;
2525
import com.google.common.base.Preconditions;
26-
import com.google.common.base.Predicate;
2726
import com.google.common.base.Strings;
2827
import com.google.common.base.Throwables;
28+
import com.google.common.collect.HashBiMap;
2929
import com.google.common.collect.ImmutableMap;
3030
import com.google.common.collect.Lists;
3131
import com.google.common.collect.Maps;
@@ -36,13 +36,15 @@
3636
import javax.validation.constraints.NotNull;
3737
import java.io.ByteArrayOutputStream;
3838
import java.io.IOException;
39+
import java.util.Collections;
3940
import java.util.List;
4041
import java.util.Map;
4142

4243
@JsonTypeName("map")
4344
public class MapLookupExtractor extends LookupExtractor
4445
{
4546
private final Map<String, String> map;
47+
private final Map<String, String> reverseMap;
4648

4749
private final boolean isOneToOne;
4850

@@ -52,8 +54,18 @@ public MapLookupExtractor(
5254
@JsonProperty("isOneToOne") boolean isOneToOne
5355
)
5456
{
55-
this.map = Preconditions.checkNotNull(map, "map");
57+
58+
Preconditions.checkNotNull(map, "map");
59+
5660
this.isOneToOne = isOneToOne;
61+
62+
if (this.isOneToOne) {
63+
this.map = HashBiMap.create(map);
64+
this.reverseMap = ((HashBiMap<String, String>) this.map).inverse();
65+
} else {
66+
this.map = map;
67+
this.reverseMap = null;
68+
}
5769
}
5870

5971
@JsonProperty
@@ -72,14 +84,14 @@ public String apply(@NotNull String val)
7284
@Override
7385
public List<String> unapply(final String value)
7486
{
75-
return Lists.newArrayList(Maps.filterKeys(map, new Predicate<String>()
76-
{
77-
@Override public boolean apply(@Nullable String key)
78-
{
79-
return map.get(key).equals(Strings.nullToEmpty(value));
80-
}
81-
}).keySet());
87+
String valueToLookup = Strings.nullToEmpty(value);
8288

89+
if (this.reverseMap != null) {
90+
String val = this.reverseMap.get(valueToLookup);
91+
return (val != null) ? Collections.singletonList(val) : Collections.emptyList();
92+
} else {
93+
return Lists.newArrayList(Maps.filterKeys(map, key -> map.get(key).equals(valueToLookup)).keySet());
94+
}
8395
}
8496

8597
@Override

processing/src/main/java/io/druid/query/lookup/LookupExtractor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public Map<String, String> applyAll(Iterable<String> keys)
7474
* Null and empty are considered to be the same value = nullToEmpty(value)
7575
*
7676
* @return the list of keys that maps to value or empty list.
77-
* Note that for the case of a none existing value in the lookup we have to cases either return an empty list OR list with null element.
77+
* Note that for the case of a none existing value in the lookup we have two cases either return an empty list OR list with null element.
7878
* returning an empty list implies that user want to ignore such a lookup value.
7979
* In the other hand returning a list with the null element implies user want to map the none existing value to the key null.
8080
*/

processing/src/test/java/io/druid/query/extraction/MapLookupExtractorTest.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public class MapLookupExtractorTest
3535
private final MapLookupExtractor fn = new MapLookupExtractor(lookupMap, false);
3636

3737
@Test
38-
public void testUnApply()
38+
public void testNonInjectiveUnApply()
3939
{
4040
Assert.assertEquals(Arrays.asList("foo"), fn.unapply("bar"));
4141
Assert.assertEquals(Sets.newHashSet("null", "empty String"), Sets.newHashSet(fn.unapply("")));
@@ -46,6 +46,23 @@ public void testUnApply()
4646
Assert.assertEquals("not existing value returns empty list", Collections.EMPTY_LIST, fn.unapply("not There"));
4747
}
4848

49+
@Test
50+
public void testInjectiveUnApply()
51+
{
52+
MapLookupExtractor injectiveFn = new MapLookupExtractor(
53+
ImmutableMap.of("foo", "bar", "null", "", "", "empty_string"), true
54+
);
55+
56+
Assert.assertEquals(Arrays.asList("foo"), injectiveFn.unapply("bar"));
57+
Assert.assertEquals(
58+
"Null value should be equal to empty string",
59+
Sets.newHashSet("null"),
60+
Sets.newHashSet(injectiveFn.unapply((String) null))
61+
);
62+
Assert.assertEquals(Sets.newHashSet(""), Sets.newHashSet(injectiveFn.unapply("empty_string")));
63+
Assert.assertEquals("not existing value returns empty list", Collections.EMPTY_LIST, injectiveFn.unapply("not There"));
64+
}
65+
4966
@Test
5067
public void testGetMap()
5168
{

0 commit comments

Comments
 (0)