-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LUCENE-8868: New storing strategy for BKD tree leaves with low cardinality #730
Changes from 9 commits
47df1d4
1f8738e
69f4d4e
033564b
15e8958
f39370c
80ee25d
f904dfb
918d399
2f39af3
6eaf5a1
13f1f51
b9c271a
c8103c7
5054ff4
885ea93
a1471fe
9f453d7
e6aa3e6
30f36ce
cc08c5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -441,8 +441,15 @@ int readDocIDs(IndexInput in, long blockFP, int[] docIDs) throws IOException { | |
|
||
void visitDocValues(int[] commonPrefixLengths, byte[] scratchDataPackedValue, byte[] scratchMinIndexPackedValue, byte[] scratchMaxIndexPackedValue, | ||
IndexInput in, int[] docIDs, int count, IntersectVisitor visitor) throws IOException { | ||
if (version >= BKDWriter.VERSION_LOW_CARDINALITY_LEAVES) { | ||
visitDocValuesWithCardinality(commonPrefixLengths, scratchDataPackedValue, scratchMinIndexPackedValue, scratchMaxIndexPackedValue, in, docIDs, count, visitor); | ||
} else { | ||
visitDocValuesNoCardinality(commonPrefixLengths, scratchDataPackedValue, scratchMinIndexPackedValue, scratchMaxIndexPackedValue, in, docIDs, count, visitor); | ||
} | ||
} | ||
|
||
|
||
void visitDocValuesNoCardinality(int[] commonPrefixLengths, byte[] scratchDataPackedValue, byte[] scratchMinIndexPackedValue, byte[] scratchMaxIndexPackedValue, | ||
IndexInput in, int[] docIDs, int count, IntersectVisitor visitor) throws IOException { | ||
readCommonPrefixes(commonPrefixLengths, scratchDataPackedValue, in); | ||
|
||
if (numIndexDims != 1 && version >= BKDWriter.VERSION_LEAF_STORES_BOUNDS) { | ||
|
@@ -480,12 +487,62 @@ void visitDocValues(int[] commonPrefixLengths, byte[] scratchDataPackedValue, by | |
int compressedDim = readCompressedDim(in); | ||
|
||
if (compressedDim == -1) { | ||
visitRawDocValues(commonPrefixLengths, scratchDataPackedValue, in, docIDs, count, visitor); | ||
visitUniqueRawDocValues(scratchDataPackedValue, docIDs, count, visitor); | ||
} else { | ||
visitCompressedDocValues(commonPrefixLengths, scratchDataPackedValue, in, docIDs, count, visitor, compressedDim); | ||
} | ||
} | ||
|
||
void visitDocValuesWithCardinality(int[] commonPrefixLengths, byte[] scratchDataPackedValue, byte[] scratchMinIndexPackedValue, byte[] scratchMaxIndexPackedValue, | ||
IndexInput in, int[] docIDs, int count, IntersectVisitor visitor) throws IOException { | ||
|
||
readCommonPrefixes(commonPrefixLengths, scratchDataPackedValue, in); | ||
int compressedDim = readCompressedDim(in); | ||
if (compressedDim == -1) { | ||
//all values are the same | ||
visitor.grow(count); | ||
visitUniqueRawDocValues(scratchDataPackedValue, docIDs, count, visitor); | ||
} else { | ||
if (numIndexDims != 1 && version >= BKDWriter.VERSION_LEAF_STORES_BOUNDS) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the version check shouldn't be necessary since this method is only called when version gte VERSION_LOW_CARDINALITY_LEAVES? |
||
byte[] minPackedValue = scratchMinIndexPackedValue; | ||
System.arraycopy(scratchDataPackedValue, 0, minPackedValue, 0, packedIndexBytesLength); | ||
byte[] maxPackedValue = scratchMaxIndexPackedValue; | ||
//Copy common prefixes before reading adjusted | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you leave a space between the slashes and the text like we usually do in the rest of the codebase? |
||
// box | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move end of comment to the previous line? |
||
System.arraycopy(minPackedValue, 0, maxPackedValue, 0, packedIndexBytesLength); | ||
readMinMax(commonPrefixLengths, minPackedValue, maxPackedValue, in); | ||
|
||
// The index gives us range of values for each dimension, but the actual range of values | ||
// might be much more narrow than what the index told us, so we double check the relation | ||
// here, which is cheap yet might help figure out that the block either entirely matches | ||
// or does not match at all. This is especially more likely in the case that there are | ||
// multiple dimensions that have correlation, ie. splitting on one dimension also | ||
// significantly changes the range of values in another dimension. | ||
Relation r = visitor.compare(minPackedValue, maxPackedValue); | ||
if (r == Relation.CELL_OUTSIDE_QUERY) { | ||
return; | ||
} | ||
visitor.grow(count); | ||
|
||
if (r == Relation.CELL_INSIDE_QUERY) { | ||
for (int i = 0; i < count; ++i) { | ||
visitor.visit(docIDs[i]); | ||
} | ||
return; | ||
} | ||
} else { | ||
visitor.grow(count); | ||
} | ||
if (compressedDim == -2) { | ||
//low cardinality values | ||
visitSparseRawDocValues(commonPrefixLengths, scratchDataPackedValue, in, docIDs, count, visitor); | ||
} else { | ||
//high cardinality | ||
visitCompressedDocValues(commonPrefixLengths, scratchDataPackedValue, in, docIDs, count, visitor, compressedDim); | ||
} | ||
} | ||
} | ||
|
||
private void readMinMax(int[] commonPrefixLengths, byte[] minPackedValue, byte[] maxPackedValue, IndexInput in) throws IOException { | ||
for (int dim = 0; dim < numIndexDims; dim++) { | ||
int prefix = commonPrefixLengths[dim]; | ||
|
@@ -495,12 +552,27 @@ private void readMinMax(int[] commonPrefixLengths, byte[] minPackedValue, byte[] | |
} | ||
|
||
// Just read suffixes for every dimension | ||
private void visitRawDocValues(int[] commonPrefixLengths, byte[] scratchPackedValue, IndexInput in, int[] docIDs, int count, IntersectVisitor visitor) throws IOException { | ||
for (int i = 0; i < count; ++i) { | ||
private void visitSparseRawDocValues(int[] commonPrefixLengths, byte[] scratchPackedValue, IndexInput in, int[] docIDs, int count, IntersectVisitor visitor) throws IOException { | ||
int i; | ||
for (i = 0; i < count;) { | ||
int length = in.readVInt(); | ||
for(int dim=0;dim<numDataDims;dim++) { | ||
int prefix = commonPrefixLengths[dim]; | ||
in.readBytes(scratchPackedValue, dim*bytesPerDim + prefix, bytesPerDim - prefix); | ||
} | ||
for (int j = i; j < i + length; j++) { | ||
visitor.visit(docIDs[j], scratchPackedValue); | ||
} | ||
i+= length; | ||
} | ||
if (i != count) { | ||
throw new CorruptIndexException("Sub blocks do not add up to the expected count: " + count + " != " + i, in); | ||
} | ||
} | ||
|
||
// Just read suffixes for every dimension | ||
private void visitUniqueRawDocValues(byte[] scratchPackedValue, int[] docIDs, int count, IntersectVisitor visitor) throws IOException { | ||
for (int i = 0; i < count; i++) { | ||
visitor.visit(docIDs[i], scratchPackedValue); | ||
} | ||
} | ||
|
@@ -530,7 +602,7 @@ private void visitCompressedDocValues(int[] commonPrefixLengths, byte[] scratchP | |
|
||
private int readCompressedDim(IndexInput in) throws IOException { | ||
int compressedDim = in.readByte(); | ||
if (compressedDim < -1 || compressedDim >= numDataDims) { | ||
if (compressedDim < -2 || compressedDim >= numDataDims || (version < BKDWriter.VERSION_LOW_CARDINALITY_LEAVES && compressedDim == -2)) { | ||
throw new CorruptIndexException("Got compressedDim="+compressedDim, in); | ||
} | ||
return compressedDim; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,7 +80,8 @@ public class BKDWriter implements Closeable { | |
//public static final int VERSION_CURRENT = VERSION_START; | ||
public static final int VERSION_LEAF_STORES_BOUNDS = 5; | ||
public static final int VERSION_SELECTIVE_INDEXING = 6; | ||
public static final int VERSION_CURRENT = VERSION_SELECTIVE_INDEXING; | ||
public static final int VERSION_LOW_CARDINALITY_LEAVES= 7; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a space before the equal sign? There are a couple other places where spaces are missing in this PR. |
||
public static final int VERSION_CURRENT = VERSION_LOW_CARDINALITY_LEAVES; | ||
|
||
/** How many bytes each docs takes in the fixed-width offline format */ | ||
private final int bytesPerDoc; | ||
|
@@ -516,6 +517,7 @@ private class OneDimensionBKDWriter { | |
final int[] leafDocs = new int[maxPointsInLeafNode]; | ||
private long valueCount; | ||
private int leafCount; | ||
private int leafCardinality; | ||
|
||
OneDimensionBKDWriter(IndexOutput out) { | ||
if (numIndexDims != 1) { | ||
|
@@ -546,6 +548,9 @@ void add(byte[] packedValue, int docID) throws IOException { | |
assert valueInOrder(valueCount + leafCount, | ||
0, lastPackedValue, packedValue, 0, docID, lastDocID); | ||
|
||
if (leafCount == 0 || Arrays.mismatch(leafValues, (leafCount - 1) * bytesPerDim, leafCount * bytesPerDim, packedValue, 0, bytesPerDim) != -1) { | ||
leafCardinality++; | ||
} | ||
System.arraycopy(packedValue, 0, leafValues, leafCount * packedBytesLength, packedBytesLength); | ||
leafDocs[leafCount] = docID; | ||
docsSeen.set(docID); | ||
|
@@ -558,7 +563,8 @@ assert valueInOrder(valueCount + leafCount, | |
if (leafCount == maxPointsInLeafNode) { | ||
// We write a block once we hit exactly the max count ... this is different from | ||
// when we write N > 1 dimensional points where we write between max/2 and max per leaf block | ||
writeLeafBlock(); | ||
writeLeafBlock(leafCardinality); | ||
leafCardinality = 0; | ||
leafCount = 0; | ||
} | ||
|
||
|
@@ -567,7 +573,8 @@ assert valueInOrder(valueCount + leafCount, | |
|
||
public long finish() throws IOException { | ||
if (leafCount > 0) { | ||
writeLeafBlock(); | ||
writeLeafBlock(leafCardinality); | ||
leafCardinality = 0; | ||
leafCount = 0; | ||
} | ||
|
||
|
@@ -593,7 +600,7 @@ public long finish() throws IOException { | |
return indexFP; | ||
} | ||
|
||
private void writeLeafBlock() throws IOException { | ||
private void writeLeafBlock(int leafCardinality) throws IOException { | ||
assert leafCount != 0; | ||
if (valueCount == 0) { | ||
System.arraycopy(leafValues, 0, minPackedValue, 0, packedIndexBytesLength); | ||
|
@@ -613,7 +620,7 @@ private void writeLeafBlock() throws IOException { | |
int offset = (leafCount - 1) * packedBytesLength; | ||
int prefix = Arrays.mismatch(leafValues, 0, bytesPerDim, leafValues, offset, offset + bytesPerDim); | ||
if (prefix == -1) { | ||
prefix = bytesPerDim; | ||
prefix = bytesPerDim; | ||
} | ||
|
||
commonPrefixLengths[0] = prefix; | ||
|
@@ -635,7 +642,7 @@ public BytesRef apply(int i) { | |
assert valuesInOrderAndBounds(leafCount, 0, ArrayUtil.copyOfSubArray(leafValues, 0, packedBytesLength), | ||
ArrayUtil.copyOfSubArray(leafValues, (leafCount - 1) * packedBytesLength, leafCount * packedBytesLength), | ||
packedValues, leafDocs, 0); | ||
writeLeafBlockPackedValues(scratchOut, commonPrefixLengths, leafCount, 0, packedValues); | ||
writeLeafBlockPackedValues(scratchOut, commonPrefixLengths, leafCount, 0, packedValues, leafCardinality); | ||
scratchOut.copyTo(out); | ||
scratchOut.reset(); | ||
} | ||
|
@@ -1028,34 +1035,91 @@ private void writeLeafBlockDocs(DataOutput out, int[] docIDs, int start, int cou | |
DocIdsWriter.writeDocIds(docIDs, start, count, out); | ||
} | ||
|
||
private void writeLeafBlockPackedValues(DataOutput out, int[] commonPrefixLengths, int count, int sortedDim, IntFunction<BytesRef> packedValues) throws IOException { | ||
private void writeLeafBlockPackedValues(DataOutput out, int[] commonPrefixLengths, int count, int sortedDim, IntFunction<BytesRef> packedValues, int leafCardinality) throws IOException { | ||
int prefixLenSum = Arrays.stream(commonPrefixLengths).sum(); | ||
if (prefixLenSum == packedBytesLength) { | ||
// all values in this block are equal | ||
out.writeByte((byte) -1); | ||
} else { | ||
if (numIndexDims != 1) { | ||
writeActualBounds(out, commonPrefixLengths, count, packedValues); | ||
} | ||
assert commonPrefixLengths[sortedDim] < bytesPerDim; | ||
out.writeByte((byte) sortedDim); | ||
//estimate if storing the values with cardinality is cheaper than storing all values. | ||
int compressedByteOffset = sortedDim * bytesPerDim + commonPrefixLengths[sortedDim]; | ||
commonPrefixLengths[sortedDim]++; | ||
for (int i = 0; i < count; ) { | ||
// do run-length compression on the byte at compressedByteOffset | ||
int runLen = runLen(packedValues, i, Math.min(i + 0xff, count), compressedByteOffset); | ||
assert runLen <= 0xff; | ||
BytesRef first = packedValues.apply(i); | ||
byte prefixByte = first.bytes[first.offset + compressedByteOffset]; | ||
out.writeByte(prefixByte); | ||
out.writeByte((byte) runLen); | ||
writeLeafBlockPackedValuesRange(out, commonPrefixLengths, i, i + runLen, packedValues); | ||
i += runLen; | ||
assert i <= count; | ||
int highCardinalityCost; | ||
int lowCardinalityCost; | ||
if (count == leafCardinality) { | ||
//all values in this block are different | ||
highCardinalityCost = 0; | ||
lowCardinalityCost = 1; | ||
} else { | ||
//compute cost of runLen compression | ||
int numRunLens = 0; | ||
for (int i = 0; i < count; ) { | ||
// do run-length compression on the byte at compressedByteOffset | ||
int runLen = runLen(packedValues, i, Math.min(i + 0xff, count), compressedByteOffset); | ||
assert runLen <= 0xff; | ||
numRunLens++; | ||
i += runLen; | ||
} | ||
//Add cost of runLen compression | ||
highCardinalityCost = count * (packedBytesLength - prefixLenSum - 1) + 2 * numRunLens; | ||
//+1 is the byte needed for storing the cardinality | ||
lowCardinalityCost = leafCardinality * (packedBytesLength - prefixLenSum + 1); | ||
} | ||
if (lowCardinalityCost <= highCardinalityCost) { | ||
out.writeByte((byte) -2); | ||
writeLowCardinalityLeafBlockPackedValues(out, commonPrefixLengths, count, packedValues); | ||
} else { | ||
out.writeByte((byte) sortedDim); | ||
writeHighCardinalityLeafBlockPackedValues(out, commonPrefixLengths, count, sortedDim, packedValues, compressedByteOffset); | ||
} | ||
} | ||
} | ||
|
||
private void writeLowCardinalityLeafBlockPackedValues(DataOutput out, int[] commonPrefixLengths, int count, IntFunction<BytesRef> packedValues) throws IOException { | ||
if (numIndexDims != 1) { | ||
writeActualBounds(out, commonPrefixLengths, count, packedValues); | ||
} | ||
BytesRef value = packedValues.apply(0); | ||
System.arraycopy(value.bytes, value.offset, scratch1, 0, packedBytesLength); | ||
int cardinality = 1; | ||
for (int i = 1; i < count; i++) { | ||
value = packedValues.apply(i); | ||
if (Arrays.mismatch(value.bytes, value.offset, value.offset + value.length, scratch1, 0, packedBytesLength) != -1) { | ||
out.writeVInt(cardinality); | ||
for (int j = 0; j < numDataDims; j++) { | ||
out.writeBytes(scratch1, j * bytesPerDim + commonPrefixLengths[j], bytesPerDim - commonPrefixLengths[j]); | ||
} | ||
System.arraycopy(value.bytes, value.offset, scratch1, 0, packedBytesLength); | ||
cardinality = 1; | ||
} else { | ||
cardinality++; | ||
} | ||
} | ||
out.writeVInt(cardinality); | ||
for (int i = 0; i < numDataDims; i++) { | ||
out.writeBytes(scratch1, i * bytesPerDim + commonPrefixLengths[i], bytesPerDim - commonPrefixLengths[i]); | ||
} | ||
} | ||
|
||
private void writeHighCardinalityLeafBlockPackedValues(DataOutput out, int[] commonPrefixLengths, int count, int sortedDim, IntFunction<BytesRef> packedValues, int compressedByteOffset) throws IOException { | ||
if (numIndexDims != 1) { | ||
writeActualBounds(out, commonPrefixLengths, count, packedValues); | ||
} | ||
commonPrefixLengths[sortedDim]++; | ||
for (int i = 0; i < count; ) { | ||
// do run-length compression on the byte at compressedByteOffset | ||
int runLen = runLen(packedValues, i, Math.min(i + 0xff, count), compressedByteOffset); | ||
assert runLen <= 0xff; | ||
BytesRef first = packedValues.apply(i); | ||
byte prefixByte = first.bytes[first.offset + compressedByteOffset]; | ||
out.writeByte(prefixByte); | ||
out.writeByte((byte) runLen); | ||
writeLeafBlockPackedValuesRange(out, commonPrefixLengths, i, i + runLen, packedValues); | ||
i += runLen; | ||
assert i <= count; | ||
} | ||
} | ||
|
||
private void writeActualBounds(DataOutput out, int[] commonPrefixLengths, int count, IntFunction<BytesRef> packedValues) throws IOException { | ||
for (int dim = 0; dim < numIndexDims; ++dim) { | ||
int commonPrefixLength = commonPrefixLengths[dim]; | ||
|
@@ -1295,6 +1359,20 @@ private void build(int nodeID, int leafNodeOffset, | |
MutablePointsReaderUtils.sortByDim(sortedDim, bytesPerDim, commonPrefixLengths, | ||
reader, from, to, scratchBytesRef1, scratchBytesRef2); | ||
|
||
BytesRef comparator = scratchBytesRef1; | ||
BytesRef collector = scratchBytesRef2; | ||
reader.getValue(from, comparator); | ||
int leafCardinality = 1; | ||
for (int i = from + 1; i < to; ++i) { | ||
reader.getValue(i, collector); | ||
if (Arrays.mismatch(collector.bytes, collector.offset, collector.offset + packedBytesLength, | ||
comparator.bytes, comparator.offset, comparator.offset + packedBytesLength) != -1) { | ||
leafCardinality++; | ||
BytesRef scratch = collector; | ||
collector = comparator; | ||
comparator = scratch; | ||
} | ||
} | ||
// Save the block file pointer: | ||
leafBlockFPs[nodeID - leafNodeOffset] = out.getFilePointer(); | ||
|
||
|
@@ -1323,7 +1401,7 @@ public BytesRef apply(int i) { | |
}; | ||
assert valuesInOrderAndBounds(count, sortedDim, minPackedValue, maxPackedValue, packedValues, | ||
docIDs, 0); | ||
writeLeafBlockPackedValues(scratchOut, commonPrefixLengths, count, sortedDim, packedValues); | ||
writeLeafBlockPackedValues(scratchOut, commonPrefixLengths, count, sortedDim, packedValues, leafCardinality); | ||
scratchOut.copyTo(out); | ||
scratchOut.reset(); | ||
} else { | ||
|
@@ -1426,6 +1504,8 @@ private void build(int nodeID, int leafNodeOffset, | |
|
||
// sort the chosen dimension | ||
radixSelector.heapRadixSort(heapSource, from, to, sortedDim, commonPrefixLengths[sortedDim]); | ||
// compute cardinality | ||
int leafCardinality = heapSource.computeCardinality(from ,to); | ||
|
||
// Save the block file pointer: | ||
leafBlockFPs[nodeID - leafNodeOffset] = out.getFilePointer(); | ||
|
@@ -1459,7 +1539,7 @@ public BytesRef apply(int i) { | |
}; | ||
assert valuesInOrderAndBounds(count, sortedDim, minPackedValue, maxPackedValue, packedValues, | ||
heapSource.docIDs, from); | ||
writeLeafBlockPackedValues(out, commonPrefixLengths, count, sortedDim, packedValues); | ||
writeLeafBlockPackedValues(out, commonPrefixLengths, count, sortedDim, packedValues, leafCardinality); | ||
|
||
} else { | ||
// Inner node: partition/recurse | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we always call visitDocValuesWithCardinality? It seems to include the version check already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we cannot because in the previous version of the BKD tree the compressed dim byte was written after the leaf bounds. This is the reason I have to fork the version here.