Skip to content
This repository was archived by the owner on Jun 14, 2024. It is now read-only.

Commit f8202d9

Browse files
author
Chungmin Lee
committed
Data Skipping Index Part 4: BloomFilterSketch
Implement BloomFilterSketch.
1 parent 6ca6cc7 commit f8202d9

23 files changed

Lines changed: 1321 additions & 1 deletion
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (2021) The Hyperspace Project Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.hyperspace.index.dataskipping.sketch
18+
19+
import org.apache.spark.sql.catalyst.expressions.{Expression, ExprId}
20+
import org.apache.spark.sql.types.DataType
21+
22+
import com.microsoft.hyperspace.index.dataskipping.util._
23+
import com.microsoft.hyperspace.index.dataskipping.util.ArrayUtils.toArray
24+
25+
/**
26+
* Sketch based on a bloom filter for a given expression.
27+
*
28+
* Being a probabilistic structure, it is more efficient in terms of the index
29+
* data size than [[ValueListSketch]] if the number of distinct values for the
30+
* expression is large, but can be less efficient in terms of query optimization
31+
* than [[ValueListSketch]] due to false positives.
32+
*
33+
* Users can specify the target false positive rate and the expected number of
34+
* distinct values per file. These variables determine the size of the bloom
35+
* filters and thus the size of the index data.
36+
*
37+
* @param expr Expression this sketch is based on
38+
* @param fpp Target false positive rate
39+
* @param expectedDistinctCountPerFile Expected number of distinct values per file
40+
*/
41+
case class BloomFilterSketch(
42+
override val expr: String,
43+
fpp: Double,
44+
expectedDistinctCountPerFile: Long,
45+
override val dataType: Option[DataType] = None)
46+
extends SingleExprSketch[BloomFilterSketch](expr, dataType) {
47+
override def name: String = "BloomFilter"
48+
49+
override def toString: String = s"$name($expr, $fpp, $expectedDistinctCountPerFile)"
50+
51+
override def withNewExpression(newExpr: (String, Option[DataType])): BloomFilterSketch = {
52+
copy(expr = newExpr._1, dataType = newExpr._2)
53+
}
54+
55+
override def aggregateFunctions: Seq[Expression] = {
56+
BloomFilterAgg(parsedExpr, expectedDistinctCountPerFile, fpp).toAggregateExpression() :: Nil
57+
}
58+
59+
override def convertPredicate(
60+
predicate: Expression,
61+
sketchValues: Seq[Expression],
62+
nameMap: Map[ExprId, String],
63+
resolvedExprs: Seq[Expression]): Option[Expression] = {
64+
val bf = sketchValues(0)
65+
val resolvedExpr = resolvedExprs.head
66+
val dataType = resolvedExpr.dataType
67+
val exprMatcher = NormalizedExprMatcher(resolvedExpr, nameMap)
68+
val ExprEqualTo = EqualToExtractor(exprMatcher)
69+
val ExprIn = InExtractor(exprMatcher)
70+
val ExprInSet = InSetExtractor(exprMatcher)
71+
Option(predicate).collect {
72+
case ExprEqualTo(v) => BloomFilterMightContain(bf, v)
73+
case ExprIn(vs) =>
74+
BloomFilterMightContainAny(bf, toArray(vs.map(_.eval()), dataType), dataType)
75+
case ExprInSet(vs) =>
76+
BloomFilterMightContainAny(bf, toArray(vs.filter(_ != null).toSeq, dataType), dataType)
77+
}
78+
}
79+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (2021) The Hyperspace Project Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.hyperspace.index.dataskipping.util
18+
19+
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
20+
21+
import org.apache.spark.sql.catalyst.InternalRow
22+
import org.apache.spark.sql.catalyst.expressions.Expression
23+
import org.apache.spark.sql.catalyst.expressions.aggregate.TypedImperativeAggregate
24+
import org.apache.spark.sql.types.DataType
25+
import org.apache.spark.util.sketch.BloomFilter
26+
27+
/**
28+
* Aggregation function that collects elements in a bloom filter.
29+
*/
30+
case class BloomFilterAgg(
31+
child: Expression,
32+
expectedNumItems: Long,
33+
fpp: Double,
34+
override val mutableAggBufferOffset: Int = 0,
35+
override val inputAggBufferOffset: Int = 0)
36+
extends TypedImperativeAggregate[BloomFilter] {
37+
38+
private def bloomFilterEncoder = BloomFilterEncoderProvider.defaultEncoder
39+
40+
override def prettyName: String = "bloom_filter"
41+
42+
override def dataType: DataType = bloomFilterEncoder.dataType
43+
44+
override def nullable: Boolean = false
45+
46+
override def children: Seq[Expression] = Seq(child)
47+
48+
override def createAggregationBuffer(): BloomFilter = {
49+
BloomFilter.create(expectedNumItems, fpp)
50+
}
51+
52+
override def update(buffer: BloomFilter, input: InternalRow): BloomFilter = {
53+
val value = child.eval(input)
54+
if (value != null) {
55+
BloomFilterUtils.put(buffer, value, child.dataType)
56+
}
57+
buffer
58+
}
59+
60+
override def merge(buffer: BloomFilter, input: BloomFilter): BloomFilter = {
61+
buffer.mergeInPlace(input)
62+
buffer
63+
}
64+
65+
override def eval(buffer: BloomFilter): Any = bloomFilterEncoder.encode(buffer)
66+
67+
override def serialize(buffer: BloomFilter): Array[Byte] = {
68+
val out = new ByteArrayOutputStream()
69+
buffer.writeTo(out)
70+
out.toByteArray
71+
}
72+
73+
override def deserialize(bytes: Array[Byte]): BloomFilter = {
74+
val in = new ByteArrayInputStream(bytes)
75+
BloomFilter.readFrom(in)
76+
}
77+
78+
override def withNewMutableAggBufferOffset(newOffset: Int): BloomFilterAgg =
79+
copy(mutableAggBufferOffset = newOffset)
80+
81+
override def withNewInputAggBufferOffset(newOffset: Int): BloomFilterAgg =
82+
copy(inputAggBufferOffset = newOffset)
83+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (2021) The Hyperspace Project Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.hyperspace.index.dataskipping.util
18+
19+
import org.apache.spark.sql.types.DataType
20+
import org.apache.spark.util.sketch.BloomFilter
21+
22+
/**
23+
* Defines how [[BloomFilter]] should be represented in the Spark DataFrame.
24+
*/
25+
trait BloomFilterEncoder {
26+
27+
/**
28+
* Returns the data type of the value in the DataFrame representing [[BloomFilter]].
29+
*/
30+
def dataType: DataType
31+
32+
/**
33+
* Returns a value representing the given [[BloomFilter]]
34+
* that can be put in the [[InternalRow]].
35+
*/
36+
def encode(bf: BloomFilter): Any
37+
38+
/**
39+
* Returns a [[BloomFilter]] from the value in the DataFrame.
40+
*/
41+
def decode(value: Any): BloomFilter
42+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (2021) The Hyperspace Project Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.hyperspace.index.dataskipping.util
18+
19+
/**
20+
* Provides the default implementation of [[BloomFilterEncoder]].
21+
*/
22+
object BloomFilterEncoderProvider {
23+
24+
/**
25+
* Returns the default encoder.
26+
*
27+
* It should return a singleton object declared as "object".
28+
*/
29+
def defaultEncoder: BloomFilterEncoder = FastBloomFilterEncoder
30+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (2021) The Hyperspace Project Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.hyperspace.index.dataskipping.util
18+
19+
import org.apache.spark.sql.catalyst.InternalRow
20+
import org.apache.spark.sql.catalyst.expressions.{BinaryExpression, Expression, Predicate}
21+
import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, ExprCode, FalseLiteral}
22+
import org.apache.spark.sql.catalyst.expressions.codegen.Block._
23+
24+
/**
25+
* Returns true if the bloom filter (left) might contain the value (right).
26+
*
27+
* The bloom filter and the value must not be null.
28+
*/
29+
case class BloomFilterMightContain(left: Expression, right: Expression)
30+
extends BinaryExpression
31+
with Predicate {
32+
33+
override def prettyName: String = "bloom_filter_might_contain"
34+
35+
override def nullable: Boolean = false
36+
37+
override def eval(input: InternalRow): Boolean = {
38+
val bfData = left.eval(input)
39+
val bf = BloomFilterEncoderProvider.defaultEncoder.decode(bfData)
40+
val value = right.eval(input)
41+
BloomFilterUtils.mightContain(bf, value, right.dataType)
42+
}
43+
44+
override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
45+
val leftGen = left.genCode(ctx)
46+
val rightGen = right.genCode(ctx)
47+
val bloomFilterEncoder =
48+
BloomFilterEncoderProvider.defaultEncoder.getClass.getCanonicalName.stripSuffix("$")
49+
val bf = s"$bloomFilterEncoder.decode(${leftGen.value})"
50+
val result = s"${BloomFilterUtils.mightContainCodegen(bf, rightGen.value, right.dataType)}"
51+
ev.copy(
52+
code = code"""
53+
${leftGen.code}
54+
${rightGen.code}
55+
boolean ${ev.value} = $result;""",
56+
isNull = FalseLiteral)
57+
}
58+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (2021) The Hyperspace Project Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.hyperspace.index.dataskipping.util
18+
19+
import scala.collection.immutable.TreeSet
20+
21+
import org.apache.spark.sql.catalyst.InternalRow
22+
import org.apache.spark.sql.catalyst.expressions.{Expression, Predicate, UnaryExpression}
23+
import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, CodeGenerator, ExprCode, FalseLiteral}
24+
import org.apache.spark.sql.catalyst.expressions.codegen.Block._
25+
import org.apache.spark.sql.types.DataType
26+
import org.apache.spark.util.sketch.BloomFilter
27+
28+
/**
29+
* Returns true if the bloom filter (child) might contain one of the values.
30+
*
31+
* The bloom filter must not be null.
32+
* The values must be an array without nulls.
33+
* If the element type can be represented as a primitive type in Scala,
34+
* then the array must be an array of the primitive type.
35+
*/
36+
case class BloomFilterMightContainAny(child: Expression, values: Any, elementType: DataType)
37+
extends UnaryExpression
38+
with Predicate {
39+
40+
override def prettyName: String = "bloom_filter_might_contain_any"
41+
42+
override def nullable: Boolean = false
43+
44+
override def eval(input: InternalRow): Boolean = {
45+
val bfData = child.eval(input)
46+
val bf = BloomFilterEncoderProvider.defaultEncoder.decode(bfData)
47+
values
48+
.asInstanceOf[Array[_]]
49+
.exists(BloomFilterUtils.mightContain(bf, _, elementType))
50+
}
51+
52+
override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
53+
val childGen = child.genCode(ctx);
54+
val bloomFilterEncoder =
55+
BloomFilterEncoderProvider.defaultEncoder.getClass.getCanonicalName.stripSuffix("$")
56+
val bf = ctx.freshName("bf")
57+
val bfType = classOf[BloomFilter].getCanonicalName
58+
val javaType = CodeGenerator.javaType(elementType)
59+
val arrayType = if (values.isInstanceOf[Array[Any]]) "java.lang.Object[]" else s"$javaType[]"
60+
val valuesRef = ctx.addReferenceObj("values", values, arrayType)
61+
val valuesArray = ctx.freshName("values")
62+
val i = ctx.freshName("i")
63+
val mightContain =
64+
BloomFilterUtils.mightContainCodegen(bf, s"($javaType) $valuesArray[$i]", elementType)
65+
val resultCode =
66+
s"""
67+
|$bfType $bf = $bloomFilterEncoder.decode(${childGen.value});
68+
|$arrayType $valuesArray = $valuesRef;
69+
|for (int $i = 0; $i < $valuesArray.length; $i++) {
70+
| if ($mightContain) {
71+
| ${ev.value} = true;
72+
| break;
73+
| }
74+
|}
75+
""".stripMargin
76+
ev.copy(
77+
code = code"""
78+
${childGen.code}
79+
boolean ${ev.value} = false;
80+
$resultCode""",
81+
isNull = FalseLiteral)
82+
}
83+
84+
override def equals(that: Any): Boolean = {
85+
that match {
86+
case BloomFilterMightContainAny(thatChild, thatValues, thatElementType) =>
87+
child == thatChild &&
88+
values.asInstanceOf[Array[_]].sameElements(thatValues.asInstanceOf[Array[_]]) &&
89+
elementType == thatElementType
90+
case _ => false
91+
}
92+
}
93+
94+
override def hashCode: Int = {
95+
(child, values.asInstanceOf[Array[_]].toSeq, elementType).hashCode
96+
}
97+
}

0 commit comments

Comments
 (0)