You are viewing a plain text version of this content. The canonical link for it is here.
Posted to pr@cassandra.apache.org by "mike-tr-adamson (via GitHub)" <gi...@apache.org> on 2023/06/12 10:47:44 UTC
[GitHub] [cassandra] mike-tr-adamson opened a new pull request, #2409: Cassandra 18067
mike-tr-adamson opened a new pull request, #2409:
URL: https://github.com/apache/cassandra/pull/2409
[Numeric on-disk index write and search. Includes:](https://github.com/apache/cassandra/commit/68efba17fd4c1c6a8fa8fcf6874d82916ec637b0)
- The disk/v1/kdtree package containing the
kdtree writer and reader
- The implementation code to tie these into
the existing read and write paths. The main parts
of this are the NumericIndexWriter and the
NumericIndexSegmentSearcher
- Additional testing for the new code
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242162605
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BlockBalancedTreeWriter}). That allows us to skip reading the leaves, instead
+ * just order leaf blocks by their offset in the index file, and correlate them with buffered posting lists.
+ */
+public class BlockBalancedTreePostingsWriter implements TraversingBlockBalancedTreeReader.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
Review Comment:
It doesn't make sense having this or the `indexContext` here anyway. They are both only used in `finish`. If we got rid of these we could get rid of the constructor as well. That then makes the `LeafCallback` field very clearly `leafPostings`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239699548
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
Review Comment:
`BlockBalancedTreeReaderTest` looks to be the test that hits most of the conditions. It definitely hits this one and does create multi-leaf trees.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227228295
##########
src/java/org/apache/cassandra/index/sai/QueryContext.java:
##########
@@ -49,6 +49,12 @@
public long trieSegmentsHit = 0;
+ public long bkdPostingListsHit = 0;
+ public long bkdSegmentsHit = 0;
+
Review Comment:
nit: I guess we'll figure out whether we keep the "bkd" naming/prefix, but either way, perhaps we should group trie stuff together here in a block instead of the `bkd` stuff being sandwiched between segment hits and posting skips/decodes.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227273378
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java:
##########
@@ -71,10 +73,48 @@
private ByteBuffer minTerm;
private ByteBuffer maxTerm;
+ final AbstractType<?> termComparator;
long totalBytesAllocated;
int rowCount = 0;
int maxSegmentRowId = -1;
+ public static class KDTreeSegmentBuilder extends SegmentBuilder
+ {
+ protected final byte[] buffer;
+ private final BKDTreeRamBuffer kdTreeRamBuffer;
+
+ public KDTreeSegmentBuilder(AbstractType<?> termComparator, NamedMemoryLimiter limiter)
+ {
+ super(termComparator, limiter);
+
+ int typeSize = TypeUtil.fixedSizeOf(termComparator);
+ this.kdTreeRamBuffer = new BKDTreeRamBuffer(typeSize);
+ this.buffer = new byte[typeSize];
+ this.totalBytesAllocated = this.kdTreeRamBuffer.ramBytesUsed();
+ }
+
+ public boolean isEmpty()
+ {
+ return kdTreeRamBuffer.numRows() == 0;
+ }
+
+ protected long addInternal(ByteBuffer term, int segmentRowId)
+ {
+ TypeUtil.toComparableBytes(term, termComparator, buffer);
+ return kdTreeRamBuffer.addPackedValue(segmentRowId, new BytesRef(buffer));
+ }
+
+ @Override
+ protected SegmentMetadata.ComponentMetadataMap flushInternal(IndexDescriptor indexDescriptor, IndexContext indexContext) throws IOException
+ {
+ NumericIndexWriter writer = new NumericIndexWriter(indexDescriptor,
+ indexContext,
+ TypeUtil.fixedSizeOf(termComparator),
+ maxSegmentRowId);
+ return writer.writeAll(kdTreeRamBuffer.asPointValues());
+ }
+ }
+
public static class RAMStringSegmentBuilder extends SegmentBuilder
{
final RAMStringIndexer ramIndexer;
Review Comment:
nit: Line 247 below needs a `<p>`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#issuecomment-1613510474
@maedhroz @adelapena I have rebased this branch to bring in the Lucene 9 changes.
This caused a slight issue with the bbtree. Changes to the `PointValues` class were significant, and since the Lucene 9 implementation of the `BKDWriter` has removed the need to sort incoming values, I have changed the `BlockBalancedTreeRamBuffer` to provide sorted values to the `BlockBalancedTreeWriter`.
This means that I have effectively implemented [CASSANDRA-18598](https://issues.apache.org/jira/browse/CASSANDRA-18598).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228310364
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible
+ * tree levels.
+ *
+ * Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
Review Comment:
Makes sense to me. In the end, balanced vs prefix tree (aka trie) is clear and brief enough. It's the `block` part what makes the names uncomfortably long and, as mentioned above, that can be omitted at least for var names.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228451742
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.stream.IntStream;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link IntersectingPointValues}.
+ */
+public class BlockBalancedTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder rowIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BlockBalancedTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+
+ rowIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(rowIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ assert value.length == packedValue.length : "The value has length=" + value.length + " but should be " + packedValue.length;
+
+ long startingBytesUsed = bytesUsed.get();
+ long startingRowIDsBytesUsed = rowIDsBuilder.ramBytesUsed();
+
+ rowIDsBuilder.add(segmentRowId);
+ bytes.append(value);
+
+ if (segmentRowId != lastSegmentRowID)
+ {
+ numRows++;
+ lastSegmentRowID = segmentRowId;
+ }
+
+ numPoints++;
+
+ long rowIDsAllocatedBytes = rowIDsBuilder.ramBytesUsed() - startingRowIDsBytesUsed;
+ long endingBytesAllocated = bytesUsed.addAndGet(rowIDsAllocatedBytes);
+
+ return endingBytesAllocated - startingBytesUsed;
+ }
+
+ public IntersectingPointValues asPointValues()
+ {
+ ensureOpen();
+ // building packed longs is destructive
+ closed = true;
+ final PackedLongValues rowIDs = rowIDsBuilder.build();
+ return new IntersectingPointValues()
+ {
+ final int[] ords = IntStream.range(0, numPoints).toArray();
+
+ @Override
+ public void getValue(int i, BytesRef bytesRef)
+ {
+ final long offset = (long) packedValue.length * (long) ords[i];
+ bytesRef.length = packedValue.length;
+ bytes.setRawBytesRef(bytesRef, offset);
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ final long offset = (long) packedValue.length * (long) ords[i] + (long) k;
+
+ return bytes.readByte(offset);
+ }
+
+ @Override
+ public int getDocID(int i)
+ {
+ return Math.toIntExact(rowIDs.get(ords[i]));
+ }
+
+ @Override
+ public void swap(int i, int j)
+ {
+ int tmp = ords[i];
+ ords[i] = ords[j];
+ ords[j] = tmp;
+ }
+
+ @Override
+ public void intersect(IntersectVisitor visitor) throws IOException
+ {
+ final BytesRef scratch = new BytesRef();
+ for (int i = 0; i < numPoints; i++)
+ {
+ getValue(i, scratch);
+ assert scratch.length == packedValue.length;
+ System.arraycopy(scratch.bytes, scratch.offset, packedValue, 0, packedValue.length);
+ visitor.visit(getDocID(i), packedValue);
+ }
+ }
+
+ @Override
+ public int getBytesPerDimension()
+ {
+ return packedValue.length;
+ }
+
+ @Override
+ public long size()
+ {
+ return numPoints;
+ }
+
+ @Override
+ public int getDocCount()
+ {
+ return numRows;
+ }
+ };
+ }
+
+ private void ensureOpen()
+ {
+ Preconditions.checkState(!closed, "Expected open buffer.");
Review Comment:
super nit: This could probably be an assertion as well, given it would indicate programmer error, but not super important...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228503029
##########
src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java:
##########
@@ -235,7 +235,7 @@ public int openFilesPerSSTableIndex()
@Override
public int openFilesPerColumnIndex(IndexContext indexContext)
{
- // For the V1 format there are always 2 open files per index - index (kdtree or terms) + postings
+ // For the V1 format there are always 2 open files per index - index (balanced tree or terms) + postings
Review Comment:
For the `bbtree`, it's just the auxiliary postings in the second file, right?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#issuecomment-1611726596
@maedhroz @adelapena I have pushed changes for the remainder of the review apart from bringing in some missing tests. I will get them in tomorrow.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1246360901
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final TraversalState state;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInput indexInput = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(indexInput);
+ indexInput.seek(treeIndexRoot);
+
+ state = new TraversalState(indexInput);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return state.memoryUsage;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ state.reset();
+ traverse(callback, new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, IntArrayList pathToRoot)
+ {
+ if (state.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (state.nodeExists())
+ {
+ callback.onLeaf(state.nodeID, state.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(state.nodeID);
+
+ state.pushLeft();
+ traverse(callback, currentPath);
+ state.pop();
+
+ state.pushRight();
+ traverse(callback, currentPath);
+ state.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ /**
+ * This maintains the state for a traversal of the packed index. It is loaded once and can be resused
+ * by calling the reset method.
+ * <p>
+ * The packed index is a packed representation of a balanced tree and takes the form of a packed array of
+ * file pointer / split value pairs. Both the file pointers and split values are prefix compressed by tree level
+ * requiring us to maintain a stack of values for each level in the tree. The stack size is always the tree depth.
+ * <p>
+ * The tree is traversed by recursively following the left and then right subtrees under the current node. For the
+ * following tree (split values in square brackets):
+ * <pre>
+ * 1[16]
+ * / \
+ * / \
+ * 2[8] 3[24]
+ * / \ / \
+ * 4 5 6 7
+ * </pre>
+ * The traversal will be 1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 with nodes 4, 5, 6 & 7 being leaf nodes.
+ * <p>
+ * Assuming the full range of values in the tree is 0 -> 32, the non-leaf nodes will represent the following
+ * values:
+ * <pre>
+ * 1[0-32]
+ * / \
+ * 2[0-16] 3[16-32]
+ * </pre>
+ */
+ final static class TraversalState
+ {
+ final int bytesPerValue;
+ final int numLeaves;
+ final int treeDepth;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final long memoryUsage;
+
+ // used to read the packed index byte[]
+ final ByteArrayDataInput dataInput;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
Review Comment:
Correct. The stacks are slightly confusing and I have been trying to come up with a better explanation for them. They are very rooted to the traversal pattern.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245799130
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final TraversalState state;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInput indexInput = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(indexInput);
+ indexInput.seek(treeIndexRoot);
+
+ state = new TraversalState(indexInput);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return state.memoryUsage;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ state.reset();
+ traverse(callback, new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, IntArrayList pathToRoot)
+ {
+ if (state.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (state.nodeExists())
+ {
+ callback.onLeaf(state.nodeID, state.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(state.nodeID);
+
+ state.pushLeft();
+ traverse(callback, currentPath);
+ state.pop();
+
+ state.pushRight();
+ traverse(callback, currentPath);
+ state.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ /**
+ * This maintains the state for a traversal of the packed index. It is loaded once and can be resused
+ * by calling the reset method.
+ * <p>
+ * The packed index is a packed representation of a balanced tree and takes the form of a packed array of
+ * file pointer / split value pairs. Both the file pointers and split values are prefix compressed by tree level
+ * requiring us to maintain a stack of values for each level in the tree. The stack size is always the tree depth.
+ * <p>
+ * The tree is traversed by recursively following the left and then right subtrees under the current node. For the
+ * following tree (split values in square brackets):
+ * <pre>
+ * 1[16]
+ * / \
+ * / \
+ * 2[8] 3[24]
+ * / \ / \
+ * 4 5 6 7
+ * </pre>
+ * The traversal will be 1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 with nodes 4, 5, 6 & 7 being leaf nodes.
+ * <p>
+ * Assuming the full range of values in the tree is 0 -> 32, the non-leaf nodes will represent the following
+ * values:
+ * <pre>
+ * 1[0-32]
+ * / \
+ * 2[0-16] 3[16-32]
+ * </pre>
+ */
+ final static class TraversalState
+ {
+ final int bytesPerValue;
+ final int numLeaves;
+ final int treeDepth;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final long memoryUsage;
+
+ // used to read the packed index byte[]
+ final ByteArrayDataInput dataInput;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
Review Comment:
When we say "for each level", what we mean is "for the sub-trees rooted at the nodes on our current path"?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232842369
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+
+public class BlockBalancedTreeQueries
+{
+ private static final BlockBalancedTreeReader.IntersectVisitor MATCH_ALL = new BlockBalancedTreeReader.IntersectVisitor()
+ {
+ @Override
+ public boolean visit(byte[] packedValue)
+ {
+ return true;
+ }
+
+ @Override
+ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ return Relation.CELL_INSIDE_QUERY;
+ }
+ };
+
+ public static BlockBalancedTreeReader.IntersectVisitor balancedTreeQueryFrom(Expression expression, int bytesPerValue)
+ {
+ if (expression.lower == null && expression.upper == null)
+ {
+ return MATCH_ALL;
+ }
+
+ Bound lower = null ;
+ if (expression.lower != null)
+ {
+ final byte[] lowerBound = toComparableBytes(bytesPerValue, expression.lower.value.encoded, expression.validator);
+ lower = new Bound(lowerBound, !expression.lower.inclusive);
+ }
+
+ Bound upper = null;
+ if (expression.upper != null)
+ {
+ final byte[] upperBound = toComparableBytes(bytesPerValue, expression.upper.value.encoded, expression.validator);
+ upper = new Bound(upperBound, !expression.upper.inclusive);
+ }
+
+ return new RangeQueryVisitor(bytesPerValue, lower, upper);
+ }
+
+ private static byte[] toComparableBytes(int bytesPerDim, ByteBuffer value, AbstractType<?> type)
+ {
+ byte[] buffer = new byte[TypeUtil.fixedSizeOf(type)];
+ assert buffer.length == bytesPerDim;
+ TypeUtil.toComparableBytes(value, type, buffer);
+ return buffer;
+ }
+
+ private static abstract class RangeQuery implements BlockBalancedTreeReader.IntersectVisitor
Review Comment:
`BlockBalancedTreeReader.IntersectVisitor` -> `RangeQuery` -> `RangeQueryVisitor`
Do we need `RangeQuery`? Seems like `RangeQueryVisitor` is the only subclass?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232580517
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsIndex.java:
##########
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import com.carrotsearch.hppc.IntLongHashMap;
+import com.carrotsearch.hppc.IntLongMap;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.RandomAccessReader;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.validate;
+
+/**
+ * Mapping between node ID and an offset to its auxiliary posting list (containing every row id from all leaves
+ * reachable from that node. See {@link BlockBalancedTreePostingsWriter}).
+ */
+class BlockBalancedTreePostingsIndex
+{
+ private final int size;
+ public final IntLongMap index = new IntLongHashMap();
+
+ BlockBalancedTreePostingsIndex(FileHandle postingsFileHandle, long filePosition) throws IOException
+ {
+ try (RandomAccessReader reader = postingsFileHandle.createReader();
+ IndexInputReader input = IndexInputReader.create(reader))
+ {
+ validate(input);
+ input.seek(filePosition);
+
+ size = input.readVInt();
+
+ for (int x = 0; x < size; x++)
+ {
+ final int node = input.readVInt();
+ final long filePointer = input.readVLong();
+
+ index.put(node, filePointer);
+ }
+ }
+ }
+
+ /**
+ * Returns <tt>true</tt> if given node ID has an auxiliary posting list.
+ */
+ boolean exists(int nodeID)
+ {
+ checkArgument(nodeID > 0);
+ return index.containsKey(nodeID);
+ }
+
+ /**
+ * Returns an offset within the balanced tree postings file to the begining of the blocks summary of given node's auxiliary
+ * posting list.
+ *
+ * @throws IllegalArgumentException when given nodeID doesn't have an auxiliary posting list. Check first with
+ * {@link #exists(int)}
+ */
+ long getPostingsFilePointer(int nodeID)
+ {
+ checkArgument(exists(nodeID));
Review Comment:
nit: The `checkArgument()` calls here and in `exists()` are interesting. It's possible both cases could avoid checking, and `assert` statements moved to the calling locations, especially for `getPostingsFilePointer()`, whose callers already call `exists()` first. On the other hand, `checkArgument()`, especially in `exists()` is probably fine if we just throw a little message in there indicating `nodeID` should be at least 1.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1235733022
##########
src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java:
##########
@@ -31,6 +31,12 @@
*/
META("Meta"),
+ /**
+ * Balanced tree written by {@code BlockBalancedTreeWriter} indexes mappings of term to one or more segment row IDs
+ * (segment row ID = SSTable row ID - segment row ID offset).
+ */
+ BALANCED_TREE("BalancedTree"),
Review Comment:
Do we need to update `V1OnDiskFormat#perColumnIndexComponents()` to include this? i.e. Return this and postings if we've got a numeric column index? Feels like this should also be failing a test, but isn't currently because `StorageAttachedIndexDDLTest#verifyRebuildCorruptedFiles()` (for example) is only lookin at literal column indexes...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242378861
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
Review Comment:
Ok, this is nothing to do with dimensions. It indicates whether all the values in a leaf are the same or not. I have changed its name and implementation to be `boolean singleValue`. I have to admit that this one took me a long time to figure out what it was trying to do.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242389156
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
Review Comment:
I would prefer to wrap these in the `DEBUG` flag we are using in the writer. This allows us to enable tree debugging during testing rather than rely on assertions which can be disabled.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239891740
##########
src/java/org/apache/cassandra/config/CassandraRelevantProperties.java:
##########
@@ -399,7 +399,19 @@ public enum CassandraRelevantProperties
SAI_LATEST_VERSION("cassandra.sai.latest_version", "aa"),
SAI_MAX_FROZEN_TERM_SIZE("cassandra.sai.max_frozen_term_size_kb", "5"),
SAI_MAX_STRING_TERM_SIZE("cassandra.sai.max_string_term_size_kb", "1"),
+
+ /** Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list */
+ SAI_MINIMUM_POSTINGS_LEAVES("cassandra.sai.minimum_postings_leaves", "64"),
+
+ /**
+ * Skip, or the sampling interval, for selecting a bkd tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ SAI_POSTINGS_SKIP("cassandra.sai.postings_skip", "3"),
+
SAI_TEST_DISABLE_TIMEOUT("cassandra.sai.test.disable.timeout", "false"),
+ SAI_TEST_ENABLE_BALANCED_TREE_DEBUG("cassandra.sai.test.enable_balanced_tree_debug", "false"),
Review Comment:
Nit: we could name this `cassandra.sai.test.balanced_tree_debug_enabled`. I think we don't have conventions about this for system properties, but `*_enabled` seems a bit more common that `enable_*`. Also, `*_enabled` is the convention for yaml config properties so it makes sense to do the same here.
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes leaf postings and auxiliary posting lists for bbtree nodes. If a node has a posting list attached,
+ * it will contain every row id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * The leaf blocks are written in value order (in the order we pass them to the {@link BlockBalancedTreeWriter}).
+ * This allows us to skip reading the leaves, instead just order leaf blocks by their offset in the index file,
+ * and correlate them with buffered posting lists.
+ */
+@NotThreadSafe
+public class BlockBalancedTreePostingsWriter implements BlockBalancedTreeWalker.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
+ private final TreeMap<Long, Integer> leafOffsetToNodeID = new TreeMap<>(Long::compareTo);
+ private final Multimap<Integer, Integer> nodeToChildLeaves = HashMultimap.create();
+
+ private final IndexContext indexContext;
+ int numNonLeafPostings = 0;
+ int numLeafPostings = 0;
+
+ BlockBalancedTreePostingsWriter(List<PackedLongValues> postings, IndexContext indexContext)
+ {
+ this.postings = postings;
+ this.indexContext = indexContext;
+ }
+
+ /**
+ * Called when a leaf node is hit as we traverse the packed index.
+ *
+ * @param leafNodeID the current leaf node ID in the packed inded
+ * @param leafBlockFP the file pointer to the on-disk leaf block
+ * @param pathToRoot the path to the root leaf above this leaf. Contains all the intermediate leaf node IDs.
+ */
+ @Override
+ public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot)
+ {
+ checkArgument(!pathToRoot.containsInt(leafNodeID));
+ checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1));
+
+ leafOffsetToNodeID.put(leafBlockFP, leafNodeID);
+ for (int i = 0; i < pathToRoot.size(); i++)
+ {
+ int level = i + 1;
+ if (isLevelEligibleForPostingList(level))
+ {
+ int nodeID = pathToRoot.get(i);
+ nodeToChildLeaves.put(nodeID, leafNodeID);
+ }
+ }
+ }
+
+ /**
+ * Writes a merged posting list for each leaf in the tree. This postings list consists of the postings
+ * associated with the leaf along with the postings associated with any leaves underneath it.
+ *
Review Comment:
Nit: add `<p>`
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+
+ in.readBytes(minPackedValue, 0, bytesPerValue);
+ in.readBytes(maxPackedValue, 0, bytesPerValue);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(packedIndex, bytesPerValue, numLeaves), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final static class PackedIndexTree
+ {
+ private final int bytesPerValue;
+ private final int numLeaves;
+
+ private final byte[][] splitPackedValueStack;
+ // used to read the packed index byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the delta we read for the node at this level is a negative offset vs. the last split;
+ // this will be true if the last time we split, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ PackedIndexTree(byte[] packedIndex, int bytesPerValue, int numLeaves)
+ {
+ this.bytesPerValue = bytesPerValue;
+ this.numLeaves = numLeaves;
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[bytesPerValue];
+ readNodeData(false);
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = true;
+ in.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = false;
+ in.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ leafNode = false;
+ }
+
+ public boolean isLeafNode()
+ {
+ return nodeID >= numLeaves;
+ }
+
+ public boolean nodeExists()
+ {
+ return nodeID - numLeaves < numLeaves;
+ }
+
+ public int getNodeID()
+ {
+ return nodeID;
+ }
+
+ public byte[] getSplitPackedValue()
+ {
+ assert !isLeafNode();
+ assert splitPackedValueStack[level] != null : "level=" + level;
+ return splitPackedValueStack[level];
+ }
+
+ public long getLeafBlockFP()
+ {
+ assert isLeafNode() : "nodeID=" + nodeID + " is not a leaf";
+ return leafBlockFPStack[level];
+ }
+
+ public byte[] getSplitValue()
+ {
+ assert !isLeafNode();
+ return splitValuesStack[level];
+ }
+
+ private void readNodeData(boolean isLeft)
+ {
+ leafBlockFPStack[level] = leafBlockFPStack[level - 1];
+
+ // read leaf block FP delta
+ if (!isLeft)
+ leafBlockFPStack[level] += in.readVLong();
+
+ leafNode = isLeafNode();
+ if (!leafNode)
+ {
+ // read prefix, firstDiffByteDelta encoded as int:
+ int code = in.readVInt();
+ int prefix = code % (1 + bytesPerValue);
+ int suffix = bytesPerValue - prefix;
+
+ if (splitValuesStack[level] == null)
+ {
+ splitValuesStack[level] = new byte[bytesPerValue];
+ }
+ System.arraycopy(splitValuesStack[level - 1], 0, splitValuesStack[level], 0, bytesPerValue);
+ if (suffix > 0)
+ {
+ int firstDiffByteDelta = code / (1 + bytesPerValue);
+ if (negativeDeltas[level])
+ firstDiffByteDelta = -firstDiffByteDelta;
+ int oldByte = splitValuesStack[level][prefix] & 0xFF;
+ splitValuesStack[level][prefix] = (byte) (oldByte + firstDiffByteDelta);
+ in.readBytes(splitValuesStack[level], prefix + 1, suffix - 1);
+ }
+
+ int leftNumBytes = nodeID * 2 < numLeaves ? in.readVInt() : 0;
+
+ leftNodePositions[level] = in.getPosition();
+ rightNodePositions[level] = leftNodePositions[level] + leftNumBytes;
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
Review Comment:
If the number of leaves is a power of two then `MathUtil.log` won't apply the flooring and we will needlessly add one. I understand what we want here is to ceil the log, instead of flooring.
I think we can achieve that without resorting to `Math.ceil(Math.log(n) / Math.log(2))` with Guava's
```java
IntMath.log2(numLeaves, RoundingMode.CEILING)
```
Or we can skip the call to Guava and just use:
```java
32 - Integer.numberOfLeadingZeros(x - 1)
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242302125
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
Review Comment:
Agree with both of these. Putting `postingLists` on the `Intersection` makes things a lot clearer and getting the number of leaves isn't any work because we already have that from the index.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242361694
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
+ // If the compressed dimension is -1 it means that all values in the block are equal
+ if (compressedDimension == -1)
+ return visitRawDocValues(count, visitor, origIndex);
+ else
+ return visitCompressedDocValues(commonPrefixLength, in, count, visitor, origIndex);
+ }
+
+ private int readCompressedDimension(IndexInput in) throws IOException
+ {
+ int compressedDimension = in.readByte();
+ if (compressedDimension != -1 && compressedDimension != 0)
+ throw new CorruptIndexException(String.format("Dimension should be in the range [-1, 0], but was %d.", compressedDimension), in);
+ return compressedDimension;
+ }
+
+ private FixedBitSet visitCompressedDocValues(int commonPrefixLength,
Review Comment:
I have removed the `Doc` element here so we have `visitValues` and `visitCompressedValues`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245788421
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final TraversalState state;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInput indexInput = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(indexInput);
+ indexInput.seek(treeIndexRoot);
+
+ state = new TraversalState(indexInput);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return state.memoryUsage;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ state.reset();
+ traverse(callback, new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, IntArrayList pathToRoot)
+ {
+ if (state.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (state.nodeExists())
+ {
+ callback.onLeaf(state.nodeID, state.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(state.nodeID);
+
+ state.pushLeft();
+ traverse(callback, currentPath);
+ state.pop();
+
+ state.pushRight();
+ traverse(callback, currentPath);
+ state.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ /**
+ * This maintains the state for a traversal of the packed index. It is loaded once and can be resused
+ * by calling the reset method.
+ * <p>
+ * The packed index is a packed representation of a balanced tree and takes the form of a packed array of
+ * file pointer / split value pairs. Both the file pointers and split values are prefix compressed by tree level
+ * requiring us to maintain a stack of values for each level in the tree. The stack size is always the tree depth.
+ * <p>
+ * The tree is traversed by recursively following the left and then right subtrees under the current node. For the
+ * following tree (split values in square brackets):
+ * <pre>
+ * 1[16]
+ * / \
+ * / \
+ * 2[8] 3[24]
+ * / \ / \
+ * 4 5 6 7
+ * </pre>
+ * The traversal will be 1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 with nodes 4, 5, 6 & 7 being leaf nodes.
+ * <p>
+ * Assuming the full range of values in the tree is 0 -> 32, the non-leaf nodes will represent the following
+ * values:
+ * <pre>
+ * 1[0-32]
+ * / \
+ * 2[0-16] 3[16-32]
+ * </pre>
+ */
+ final static class TraversalState
+ {
+ final int bytesPerValue;
+ final int numLeaves;
+ final int treeDepth;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final long memoryUsage;
+
+ // used to read the packed index byte[]
+ final ByteArrayDataInput dataInput;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ final int[] rightNodePositions;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ final byte[][] splitValuesStack;
+
+ int nodeID;
+ int level;
+
+ TraversalState(DataInput dataInput) throws IOException
+ {
+ maxPointsInLeafNode = dataInput.readVInt();
+ bytesPerValue = dataInput.readVInt();
+
+ // Read index:
+ numLeaves = dataInput.readVInt();
+ assert numLeaves > 0;
+ treeDepth = dataInput.readVInt();
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+
+ dataInput.readBytes(minPackedValue, 0, bytesPerValue);
+ dataInput.readBytes(maxPackedValue, 0, bytesPerValue);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, dataInput);
+ }
+
+ pointCount = dataInput.readVLong();
+
+ int numBytes = dataInput.readVInt();
+ byte[] packedIndex = new byte[numBytes];
+ dataInput.readBytes(packedIndex, 0, numBytes);
+
+ memoryUsage = ObjectSizes.sizeOfArray(packedIndex) + ObjectSizes.sizeOfArray(minPackedValue) + ObjectSizes.sizeOfArray(maxPackedValue);
+
+ nodeID = 1;
+ level = 0;
+ leafBlockFPStack = new long[treeDepth];
+ leftNodePositions = new int[treeDepth];
+ rightNodePositions = new int[treeDepth];
+ splitValuesStack = new byte[treeDepth][];
+ this.dataInput = new ByteArrayDataInput(packedIndex);
+ readNodeData(false);
+ }
+
+ public void reset()
+ {
+ nodeID = 1;
+ level = 0;
+ dataInput.setPosition(0);
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ dataInput.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ dataInput.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ }
+
+ public boolean isLeafNode()
Review Comment:
nit: We could also call this `atLeafNode()` if you think it reads better...fine either way
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245751325
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Specialized writer for point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ * <p>
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+ private static final int DEFAULT_POSTINGS_SIZE = 128;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxRows` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxSegmentRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("indexContext", indexContext)
+ .add("bytesPerValue", bytesPerValue)
+ .toString();
+ }
+
+ private static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> leafPostings = new ArrayList<>(DEFAULT_POSTINGS_SIZE);
+
+ public int numLeaves()
+ {
+ return leafPostings.size();
+ }
+
+ @Override
+ public void writeLeafPostings(BlockBalancedTreeWriter.RowIDAndIndex[] leafPostings, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(leafPostings[i].rowID);
+ }
+ this.leafPostings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link IntersectingPointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeCompleteSegment(IntersectingPointValues values) throws IOException
+ {
+ long treePosition;
+
+ SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap();
+
+ LeafCallback leafCallback = new LeafCallback();
+
+ try (IndexOutput treeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexContext, true))
+ {
+ // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer.
+ long treeOffset = treeOutput.getFilePointer();
+
+ treePosition = writer.write(treeOutput, values, leafCallback);
+
+ // If the treePosition is less than 0 then we didn't write any values out and the index is empty
+ if (treePosition < 0)
+ return components;
+
+ long treeLength = treeOutput.getFilePointer() - treeOffset;
+
+ Map<String, String> attributes = new LinkedHashMap<>();
+ attributes.put("max_points_in_leaf_node", Integer.toString(writer.getMaxPointsInLeafNode()));
+ attributes.put("num_leaves", Integer.toString(leafCallback.numLeaves()));
+ attributes.put("num_points", Long.toString(writer.getValueCount()));
Review Comment:
```suggestion
attributes.put("num_values", Long.toString(writer.getValueCount()));
```
?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245413379
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final class PackedIndexTree
+ {
+ private final byte[][] splitPackedValueStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ // used to read the packed byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the per-dim delta we read for the node at this level is a negative offset vs. the last split on this dim; this is a packed
+ // 2D array, i.e. to access array[level][dim] you read from negativeDeltas[level*numDims+dim]. this will be true if the last time we
+ // split on this dimension, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+ // scratch value to return from getPackedValue:
+ private final BytesRef scratch;
+
+ PackedIndexTree()
+ {
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[packedBytesLength];
+ readNodeData(false);
+ scratch = new BytesRef();
+ scratch.length = bytesPerValue;
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = true;
+ in.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = false;
+ in.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ leafNode = false;
+ }
+
+ public boolean isLeafNode()
+ {
+ return nodeID >= numLeaves;
+ }
+
+ public boolean nodeExists()
+ {
+ return nodeID - numLeaves < numLeaves;
+ }
+
+ public int getNodeID()
+ {
+ return nodeID;
+ }
+
+ public byte[] getSplitPackedValue()
+ {
+ assert !isLeafNode();
+ assert splitPackedValueStack[level] != null : "level=" + level;
+ return splitPackedValueStack[level];
+ }
+
+ public long getLeafBlockFP()
+ {
+ assert isLeafNode() : "nodeID=" + nodeID + " is not a leaf";
+ return leafBlockFPStack[level];
+ }
+
+ public BytesRef getSplitDimValue()
+ {
+ assert !isLeafNode();
+ scratch.bytes = splitValuesStack[level];
+ scratch.offset = 0;
+ return scratch;
+ }
+
+ private void readNodeData(boolean isLeft)
+ {
+ leafBlockFPStack[level] = leafBlockFPStack[level - 1];
+
+ // read leaf block FP delta
+ if (!isLeft)
Review Comment:
This is a stack and is loaded with the file pointer for the next leaf at that level. This could be a left node or a right node but the stack is initialised in the `TraversalState` constructor with `isLeft = false`, so initially, the stack is set with the file pointer for the first left leaf in the tree.
This is slightly confusing and I can't find a good way to document it. It is the responsibility of the writer to pack the index such that the file pointer deltas are read correctly as the tree is traversed.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228438109
##########
src/java/org/apache/cassandra/index/sai/metrics/ColumnQueryMetrics.java:
##########
@@ -70,6 +70,61 @@ public QueryEventListener.PostingListEventListener postingListEventListener()
}
}
+ public static class BKDIndexMetrics extends ColumnQueryMetrics implements QueryEventListener.BlockBalancedTreeEventListener
Review Comment:
TODO/nit: Still using `BKD*` here and "KDTreePostings" below...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232584449
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/NumericIndexSegmentSearcher.java:
##########
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.segment;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+
+import com.google.common.base.MoreObjects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
+import org.apache.cassandra.index.sai.disk.v1.bbtree.BlockBalancedTreeReader;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.postings.PostingList;
+
+import static org.apache.cassandra.index.sai.disk.v1.bbtree.BlockBalancedTreeQueries.balancedTreeQueryFrom;
+
+/**
+ * Executes {@link Expression}s against the balanced tree for an individual index segment.
+ */
+public class NumericIndexSegmentSearcher extends IndexSegmentSearcher
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final BlockBalancedTreeReader treeReader;
+ private final QueryEventListener.BalancedTreeEventListener perColumnEventListener;
+
+ NumericIndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory,
+ PerColumnIndexFiles perIndexFiles,
+ SegmentMetadata segmentMetadata,
+ IndexContext indexContext) throws IOException
+ {
+ super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, indexContext);
+
+ final long treePosition = metadata.getIndexRoot(IndexComponent.BALANCED_TREE);
+ assert treePosition >= 0;
+ final long postingsPosition = metadata.getIndexRoot(IndexComponent.POSTING_LISTS);
+ assert postingsPosition >= 0;
Review Comment:
Should we just throw `CorruptIndexException` or something similar in these cases. `assert` feels wrong, as this could be a result of something like file tampering?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227308047
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
Review Comment:
Are `pointNumBytes` and `packedBytesLength` straight up duplicates? Perhaps coalesce then both to a single `packedValueLength`?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229311293
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,332 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ *
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(IndexTreeTraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle indexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int leafNodeOffset;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle indexFile, long root)
+ {
+ this.indexFile = indexFile;
+
+ try (RandomAccessReader reader = indexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(root);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+ leafNodeOffset = numLeaves;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(indexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(indexFile);
+ }
+
+ void traverse(IndexTreeTraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(IndexTreeTraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ /**
+ * Copy of BKDReader#getTreeDepth()
Review Comment:
I have removed this comment as redundant because the whole thing is a copy.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228257606
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible
+ * tree levels.
+ *
+ * Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for kd-tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.OneDimensionBKDWriterCallback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a k-d tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this kd-tree in the overall SSTable kd-tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
Review Comment:
I've gone with `balancedTree` as a prefix for these. Tbh, I'm moving towards using `BalancedTree` and the class prefix and `balancedtree` as the package now.
I'm going to continue with the renaming process as is and we can look at it again when it is complete.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229780460
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Specialized writer for point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ * <p>
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
+ {
+ long treePosition;
+ SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap();
+
+ LeafCallback leafCallback = new LeafCallback();
+
+ try (IndexOutput treeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexContext, true))
+ {
+ // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer.
+ long treeOffset = treeOutput.getFilePointer();
+
+ treePosition = writer.writeField(treeOutput, values, leafCallback);
+
+ // If the treePosition is less than 0 then we didn't write any values out
+ // and the index is empty
+ if (treePosition < 0)
+ return components;
+
+ long treeLength = treeOutput.getFilePointer() - treeOffset;
+
+ Map<String, String> attributes = new LinkedHashMap<>();
+ attributes.put("max_points_in_leaf_node", Integer.toString(writer.getMaxPointsInLeafNode()));
+ attributes.put("num_leaves", Integer.toString(leafCallback.numLeaves()));
+ attributes.put("num_points", Long.toString(writer.getPointCount()));
+ attributes.put("bytes_per_dim", Long.toString(writer.getBytesPerValue()));
Review Comment:
Yes, we can but it appears that we never use these values even though they are written to metadata. I'm inclined to leave them in place though because they would make useful diagnostics for a future tool that looked at the indexes.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227229193
##########
src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java:
##########
@@ -31,6 +31,12 @@
*/
META("Meta"),
+ /**
+ * KDTree written by {@code BKDWriter} indexes mappings of term to one or more segment row IDs
+ * (segment row ID = SSTable row ID - segment row ID offset).
+ */
+ KD_TREE("KDTree"),
Review Comment:
I'd rename the component to be congruent w/ whatever we name `BKDWriter`. Either way, calling this a kd-tree doesn't strictly match reality I guess.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228799186
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
Review Comment:
```suggestion
static class RowIDAndIndex
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232962335
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
+ // If the compressed dimension is -1 it means that all values in the block are equal
+ if (compressedDimension == -1)
+ return visitRawDocValues(count, visitor, origIndex);
+ else
+ return visitCompressedDocValues(commonPrefixLength, in, count, visitor, origIndex);
+ }
+
+ private int readCompressedDimension(IndexInput in) throws IOException
+ {
+ int compressedDimension = in.readByte();
+ if (compressedDimension != -1 && compressedDimension != 0)
+ throw new CorruptIndexException(String.format("Dimension should be in the range [-1, 0], but was %d.", compressedDimension), in);
+ return compressedDimension;
+ }
+
+ private FixedBitSet visitCompressedDocValues(int commonPrefixLength,
+ IndexInput in,
+ int count,
+ IntersectVisitor visitor,
+ short[] origIndex) throws IOException
+ {
+ // the byte at `compressedByteOffset` is compressed using run-length compression,
+ // other suffix bytes are stored verbatim
+ int compressedByteOffset = commonPrefixLength;
+ commonPrefixLength++;
+ int i;
+
+ FixedBitSet fixedBitSet = new FixedBitSet(maxPointsInLeafNode);
+
+ for (i = 0; i < count; )
+ {
+ packedValue[compressedByteOffset] = in.readByte();
+ final int runLen = Byte.toUnsignedInt(in.readByte());
+ for (int j = 0; j < runLen; ++j)
+ {
+ in.readBytes(packedValue, commonPrefixLength, bytesPerValue - commonPrefixLength);
+ final int rowIDIndex = origIndex[i + j];
+ if (visitor.visit(packedValue))
+ fixedBitSet.set(rowIDIndex);
+ }
+ i += runLen;
+ }
+ if (i != count)
+ throw new CorruptIndexException(String.format("Expected %d sub-blocks but read %d.", count, i), in);
+
+ return fixedBitSet;
+ }
+
+ private FixedBitSet visitRawDocValues(int count, IntersectVisitor visitor, final short[] origIndex)
+ {
+ FixedBitSet fixedBitSet = new FixedBitSet(maxPointsInLeafNode);
+
+ // In our one-dimensional case, all the values in the leaf are the same, so we only
+ // need to visit once then set the bits for the relevant indexes
+ if (visitor.visit(packedValue))
+ {
+ for (int i = 0; i < count; ++i)
+ fixedBitSet.set(origIndex[i]);
Review Comment:
Do we need to create a new bitset at all in this method? If the single value in the leaf is included, it means we could just pass a singleton "everything matches" `BitSet` right? (...or a "nothing matches" bitset otherwise?)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232962734
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
+ // If the compressed dimension is -1 it means that all values in the block are equal
+ if (compressedDimension == -1)
+ return visitRawDocValues(count, visitor, origIndex);
+ else
+ return visitCompressedDocValues(commonPrefixLength, in, count, visitor, origIndex);
+ }
+
+ private int readCompressedDimension(IndexInput in) throws IOException
+ {
+ int compressedDimension = in.readByte();
+ if (compressedDimension != -1 && compressedDimension != 0)
+ throw new CorruptIndexException(String.format("Dimension should be in the range [-1, 0], but was %d.", compressedDimension), in);
+ return compressedDimension;
+ }
+
+ private FixedBitSet visitCompressedDocValues(int commonPrefixLength,
Review Comment:
nit: Here and in a couple other places, we might want to revisit the `DocValues` naming convention.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232962894
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
Review Comment:
```suggestion
FixedBitSet filter = buildPostingsFilter(treeInput, count, visitor, origIndex);
```
?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232864091
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+
+public class BlockBalancedTreeQueries
+{
+ private static final BlockBalancedTreeReader.IntersectVisitor MATCH_ALL = new BlockBalancedTreeReader.IntersectVisitor()
+ {
+ @Override
+ public boolean visit(byte[] packedValue)
+ {
+ return true;
+ }
+
+ @Override
+ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ return Relation.CELL_INSIDE_QUERY;
+ }
+ };
+
+ public static BlockBalancedTreeReader.IntersectVisitor balancedTreeQueryFrom(Expression expression, int bytesPerValue)
+ {
+ if (expression.lower == null && expression.upper == null)
+ {
+ return MATCH_ALL;
+ }
+
+ Bound lower = null ;
+ if (expression.lower != null)
+ {
+ final byte[] lowerBound = toComparableBytes(bytesPerValue, expression.lower.value.encoded, expression.validator);
+ lower = new Bound(lowerBound, !expression.lower.inclusive);
+ }
+
+ Bound upper = null;
+ if (expression.upper != null)
+ {
+ final byte[] upperBound = toComparableBytes(bytesPerValue, expression.upper.value.encoded, expression.validator);
+ upper = new Bound(upperBound, !expression.upper.inclusive);
+ }
+
+ return new RangeQueryVisitor(bytesPerValue, lower, upper);
+ }
+
+ private static byte[] toComparableBytes(int bytesPerDim, ByteBuffer value, AbstractType<?> type)
+ {
+ byte[] buffer = new byte[TypeUtil.fixedSizeOf(type)];
+ assert buffer.length == bytesPerDim;
+ TypeUtil.toComparableBytes(value, type, buffer);
+ return buffer;
+ }
+
+ private static abstract class RangeQuery implements BlockBalancedTreeReader.IntersectVisitor
+ {
+ final int bytesPerValue;
+
+ RangeQuery(int bytesPerValue)
+ {
+ this.bytesPerValue = bytesPerValue;
+ }
+
+ int compareUnsigned(byte[] packedValue, Bound bound)
+ {
+ return FutureArrays.compareUnsigned(packedValue, 0, bytesPerValue, bound.bound, 0, bytesPerValue);
+ }
+ }
+
+ private static class Bound
+ {
+ private final byte[] bound;
+ private final boolean exclusive;
+
+ Bound(byte[] bound, boolean exclusive)
+ {
+ this.bound = bound;
+ this.exclusive = exclusive;
+ }
+
+ boolean smallerThan(int cmp)
+ {
+ return cmp > 0 || (cmp == 0 && exclusive);
+ }
+
+ boolean greaterThan(int cmp)
+ {
+ return cmp < 0 || (cmp == 0 && exclusive);
+ }
+ }
+
+ private static class RangeQueryVisitor extends RangeQuery
+ {
+ private final Bound lower;
+ private final Bound upper;
+
+ private RangeQueryVisitor(int bytesPerValue, Bound lower, Bound upper)
+ {
+ super(bytesPerValue);
+ this.lower = lower;
+ this.upper = upper;
+ }
+
+ @Override
+ public boolean visit(byte[] packedValue)
Review Comment:
nit: Part of me feels like this method would be better named `within()` or `contains()` or something less generic :/
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239711075
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(treeOutput.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
+ int commonPrefixLength = bytesPerValue;
+ int offset = (leafValueCount - 1) * bytesPerValue;
+ for (int j = 0; j < bytesPerValue; j++)
+ {
+ if (leafValues[j] != leafValues[offset + j])
+ {
+ commonPrefixLength = j;
+ break;
+ }
+ }
+
+ assert scratchOut.getPosition() == 0;
+
+ treeOutput.writeVInt(leafValueCount);
+
+ for (int x = 0; x < leafValueCount; x++)
+ {
+ rowIDAndIndexes[x].valueOrderIndex = x;
+ rowIDAndIndexes[x].rowID = leafRowIDs[x];
+ }
+
+ final Sorter sorter = new IntroSorter()
+ {
+ RowIDAndIndex pivot;
+
+ @Override
+ protected void swap(int i, int j)
+ {
+ RowIDAndIndex o = rowIDAndIndexes[i];
+ rowIDAndIndexes[i] = rowIDAndIndexes[j];
+ rowIDAndIndexes[j] = o;
+ }
+
+ @Override
+ protected void setPivot(int i)
+ {
+ pivot = rowIDAndIndexes[i];
+ }
+
+ @Override
+ protected int comparePivot(int j)
+ {
+ return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID);
+ }
+ };
+
+ sorter.sort(0, leafValueCount);
+
+ // write leaf rowID -> orig index
+ scratchOut2.reset();
+
+ // iterate in row ID order to get the row ID index for the given value order index
+ // place into an array to be written as packed ints
+ for (int x = 0; x < leafValueCount; x++)
+ orderIndex[rowIDAndIndexes[x].valueOrderIndex] = x;
+
+ LeafOrderMap.write(orderIndex, leafValueCount, maxPointsInLeafNode - 1, scratchOut2);
+
+ treeOutput.writeVInt(scratchOut2.getPosition());
+ treeOutput.writeBytes(scratchOut2.getBytes(), 0, scratchOut2.getPosition());
+
+ callback.writeLeafDocs(rowIDAndIndexes, 0, leafValueCount);
+
+ writeCommonPrefix(scratchOut, commonPrefixLength, leafValues);
+
+ scratchBytesRef1.length = bytesPerValue;
+ scratchBytesRef1.bytes = leafValues;
+
+ IntFunction<BytesRef> packedValues = (i) -> {
+ scratchBytesRef1.offset = bytesPerValue * i;
+ return scratchBytesRef1;
+ };
+ if (DEBUG)
+ valuesInOrderAndBounds(leafValueCount,
+ ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue),
+ ArrayUtil.copyOfSubArray(leafValues, (leafValueCount - 1) * bytesPerValue, leafValueCount * bytesPerValue),
+ packedValues,
+ leafRowIDs);
+
+ writeLeafBlockPackedValues(scratchOut, commonPrefixLength, leafValueCount, packedValues);
+
+ treeOutput.writeBytes(scratchOut.getBytes(), 0, scratchOut.getPosition());
+
+ scratchOut.reset();
+ }
+
+ private void checkMaxLeafNodeCount(int numLeaves)
+ {
+ if ((1 + bytesPerValue) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH)
+ {
+ throw new IllegalStateException("too many nodes; increase maxPointsInLeafNode (currently " + maxPointsInLeafNode + ") and reindex");
Review Comment:
It would mean there were fewer leaves full stop. So fewer start values, yes.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245769967
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+ private final long maxRows;
+
+ public BlockBalancedTreeWriter(long maxRows, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxRows = maxRows;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxRows), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> leafWriter.add(packedValue, rowID));
+
+ valueCount = leafWriter.finish();
+
+ long treeFilePointer = valueCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ // There is only any point in writing the balanced tree if any values were added
+ if (treeFilePointer >= 0)
+ writeBalancedTree(treeOutput, maxPointsInLeafNode, leafWriter.leafBlockStartValues, leafWriter.leafBlockFilePointers);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return treeFilePointer;
+ }
+
+ private void writeBalancedTree(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitValues = new byte[(1 + numInnerNodes) * bytesPerValue];
+ int depth = recurseBalanceTree(1, 0, numInnerNodes, 1, splitValues, leafBlockStartValues);
Review Comment:
nit: Depth starts at 1 to count the "leaf level"?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247207487
##########
src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java:
##########
@@ -123,7 +128,6 @@ public PostingsWriter(IndexDescriptor indexDescriptor, IndexContext indexContext
private PostingsWriter(IndexOutput dataOutput, int blockSize) throws IOException
{
assert dataOutput instanceof IndexOutputWriter;
Review Comment:
Any reason not to remove this `assert` and just change the type of `dataOutput` to `IndexOutputWriter`? I tried it, and after a couple minor changes up through to `NumericIndexWriter` (where `openPerIndexOutput()` does give us an `IndexOutputWriter ) it seems to work fine.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247229085
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.db.memtable.TrieMemtable;
+import org.apache.cassandra.db.tries.InMemoryTrie;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.bytecomparable.ByteSource;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for values that provides a sorted view of itself as a {@link BlockBalancedTreeIterator}.
+ */
+@NotThreadSafe
+public class BlockBalancedTreeRamBuffer
+{
+ private final InMemoryTrie<PackedLongValues.Builder> trieBuffer;
+ private final RowIDReducer reducer;
+ private final int bytesPerValue;
+ private int numRows;
+
+ public BlockBalancedTreeRamBuffer(int bytesPerValue)
+ {
+ trieBuffer = new InMemoryTrie<>(TrieMemtable.BUFFER_TYPE);
+ reducer = new RowIDReducer();
+ this.bytesPerValue = bytesPerValue;
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long memoryUsed()
+ {
+ return trieBuffer.sizeOnHeap() + reducer.heapAllocations();
+ }
+
+ public long add(int segmentRowId, byte[] value)
+ {
+ final long initialSizeOnHeap = trieBuffer.sizeOnHeap();
+ final long reducerHeapSize = reducer.heapAllocations();
+
+ try
+ {
+ trieBuffer.putRecursive(v -> ByteSource.fixedLength(value), segmentRowId, reducer);
+ }
+ catch (InMemoryTrie.SpaceExhaustedException e)
+ {
+ throw Throwables.unchecked(e);
+ }
+
+ numRows++;
+ return (trieBuffer.sizeOnHeap() - initialSizeOnHeap) + (reducer.heapAllocations() - reducerHeapSize);
+ }
+
+ public BlockBalancedTreeIterator iterator()
+ {
+ return BlockBalancedTreeIterator.fromTrieIterator(trieBuffer.entrySet().iterator(), bytesPerValue);
+ }
+
+ private static class RowIDReducer implements InMemoryTrie.UpsertTransformer<PackedLongValues.Builder, Integer>
Review Comment:
```suggestion
private static class PostingsAccumulator implements InMemoryTrie.UpsertTransformer<PackedLongValues.Builder, Integer>
```
I know, it's a wild name...feel free to ignore. (Reducer seemed odd, because it's not like reducing a new posting and existing scalar value to a new scalar value...dunno)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247222006
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeIterator.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.index.sai.utils.TermsIterator;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * A helper class for mapping disparate iterators to a single iterator for consumption by the
+ * {@link BlockBalancedTreeWriter}
+ */
+public interface BlockBalancedTreeIterator extends Iterator<Pair<byte[], PostingList>>
Review Comment:
Thinking we could name this something like `ByteTermsIterator`, especially if we reuse the interface in some capacity for strings. Maybe a better name will become apparent once we actually do that though...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247232736
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.ResettableByteBuffersIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.Pair;
+import org.apache.lucene.store.ByteBuffersDataOutput;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+
+import static org.apache.cassandra.index.sai.postings.PostingList.END_OF_STREAM;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+
+ public BlockBalancedTreeWriter(int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the sorted values from a {@link BlockBalancedTreeIterator}.
+ * <p>
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param values The {@link BlockBalancedTreeIterator} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, BlockBalancedTreeIterator values, final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ while (values.hasNext())
+ {
+ Pair<byte[], PostingList> pair = values.next();
+ while (true)
+ {
+ long segmentRowId = pair.right.nextPosting();
+ if (segmentRowId == END_OF_STREAM)
+ break;
+ leafWriter.add(pair.left, segmentRowId);
+ }
Review Comment:
I almost thought we could make `PostingList` `Iterable`, but then though maybe avoiding possible boxing there would be best.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1243903222
##########
src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java:
##########
@@ -31,6 +31,12 @@
*/
META("Meta"),
+ /**
+ * Balanced tree written by {@code BlockBalancedTreeWriter} indexes mappings of term to one or more segment row IDs
+ * (segment row ID = SSTable row ID - segment row ID offset).
+ */
+ BALANCED_TREE("BalancedTree"),
Review Comment:
Agreed, I missed this completely. I'll make sure we have a test in place for this.
EDIT: This has just highlighted a whole raft of missing tests for the numeric indexes. I'll get all these in as well.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1240144741
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes leaf postings and auxiliary posting lists for bbtree nodes. If a node has a posting list attached,
+ * it will contain every row id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * The leaf blocks are written in value order (in the order we pass them to the {@link BlockBalancedTreeWriter}).
+ * This allows us to skip reading the leaves, instead just order leaf blocks by their offset in the index file,
+ * and correlate them with buffered posting lists.
+ */
+@NotThreadSafe
+public class BlockBalancedTreePostingsWriter implements BlockBalancedTreeWalker.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
+ private final TreeMap<Long, Integer> leafOffsetToNodeID = new TreeMap<>(Long::compareTo);
+ private final Multimap<Integer, Integer> nodeToChildLeaves = HashMultimap.create();
+
+ private final IndexContext indexContext;
+ int numNonLeafPostings = 0;
+ int numLeafPostings = 0;
+
+ BlockBalancedTreePostingsWriter(List<PackedLongValues> postings, IndexContext indexContext)
+ {
+ this.postings = postings;
+ this.indexContext = indexContext;
+ }
+
+ /**
+ * Called when a leaf node is hit as we traverse the packed index.
+ *
+ * @param leafNodeID the current leaf node ID in the packed inded
+ * @param leafBlockFP the file pointer to the on-disk leaf block
+ * @param pathToRoot the path to the root leaf above this leaf. Contains all the intermediate leaf node IDs.
+ */
+ @Override
+ public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot)
+ {
+ checkArgument(!pathToRoot.containsInt(leafNodeID));
+ checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1));
+
+ leafOffsetToNodeID.put(leafBlockFP, leafNodeID);
+ for (int i = 0; i < pathToRoot.size(); i++)
+ {
+ int level = i + 1;
+ if (isLevelEligibleForPostingList(level))
+ {
+ int nodeID = pathToRoot.get(i);
+ nodeToChildLeaves.put(nodeID, leafNodeID);
+ }
+ }
+ }
+
+ /**
+ * Writes a merged posting list for each leaf in the tree. This postings list consists of the postings
+ * associated with the leaf along with the postings associated with any leaves underneath it.
+ *
+ * After writing out the postings it writes a map of leaf node IDs to postings file pointer for all
Review Comment:
nit: The map contains both internal node IDs and leaf node IDs?
Alternate wording...
```
* After writing out the postings, it writes a map of node ID -> postings file pointer for all
* nodes with an attached postings list. It then returns the file pointer to this map.
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228974312
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class OneDimensionBKDWriter
+ {
+ private final IndexOutput out;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafDocs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafCount;
+ private long lastDocID;
+
+ OneDimensionBKDWriter(IndexOutput out, Callback callback)
+ {
+ this.out = out;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafCount, lastPackedValue, packedValue, 0, docID, lastDocID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafCount * bytesPerValue, bytesPerValue);
+ leafDocs[leafCount] = docID;
+ 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
Review Comment:
nit: I guess we don't even have to mention the `N > 1` case here...because it's just not a thing?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1226885368
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDWriter.java:
##########
@@ -0,0 +1,782 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.LongBitSet;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension.
+ * <p>
+ * Recursively builds a block KD-tree to assign all incoming points in N-dim space to smaller
+ * and smaller N-dim rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * See <a href="https://www.cs.duke.edu/~pankaj/publications/papers/bkd-sstd.pdf">this paper</a> for details.
+ *
+ * <p>This consumes heap during writing: it allocates a <code>LongBitSet(numPoints)</code>,
+ * and then uses up to the specified {@code maxMBSortInHeap} heap space for writing.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ *
+ * lucene.experimental
+ */
+public class BKDWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final LongBitSet docsSeen;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BKDWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+ docsSeen = new LongBitSet(maxDoc);
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write a field from a {@link MutablePointValues}. This way of writing
+ * points is faster than regular writes with BKDWriter#add since
Review Comment:
What method is `BKDWriter#add`?
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDReader.java:
##########
@@ -0,0 +1,470 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a multidimensional shape in byte[] space with a block KD-tree previously written with
+ * {@link BKDWriter}.
Review Comment:
Isn't `BKDWriter` unidimensional?
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDWriter.java:
##########
@@ -0,0 +1,782 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.LongBitSet;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension.
+ * <p>
+ * Recursively builds a block KD-tree to assign all incoming points in N-dim space to smaller
+ * and smaller N-dim rectangles (cells) until the number of points in a given
Review Comment:
These two lines seem to contradict each other on the number of dimensions. Maybe the second line could start with something like `Lucene's implementation recursively builds...`
##########
src/java/org/apache/cassandra/index/sai/disk/io/RAMIndexOutput.java:
##########
@@ -70,11 +75,16 @@ public void writeTo(IndexOutput externalOut) throws IOException
externalOut.writeBytes(out.getBytes(), 0, out.getPosition());
}
- public BytesRef getBytes()
+ public BytesRef getBytesRef()
{
return new BytesRef(out.getBytes(), 0, out.getPosition());
}
+ public void writeTo(byte[] bytes, int offset)
Review Comment:
Nit: the offset argument passed by the callers is always zero, so perhaps this could be simplified to `writeTo(byte[] bytes)`. Also, I would place the method right after the other `writeTo(IndexOutput)` method, so similar methods stay together.
##########
src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java:
##########
@@ -130,6 +131,23 @@ public static ByteComparable max(ByteComparable a, ByteComparable b)
return a == null ? b : (b == null || ByteComparable.compare(b, a, ByteComparable.Version.OSS50) < 0) ? a : b;
}
+ /**
+ * Returns the value length for the given {@link AbstractType}, selecting 16 for types
+ * that officially use VARIABLE_LENGTH but are, in fact, of a fixed length.
+ */
+ public static int fixedSizeOf(AbstractType<?> type)
+ {
+ if (type.isValueLengthFixed())
+ return type.valueLengthIfFixed();
+ else if (isInetAddress(type))
+ return 16;
+ else if (isBigInteger(type))
+ return 20;
+ else if (type instanceof DecimalType)
Review Comment:
I think this can use `isBigDecimal(type)`.
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java:
##########
@@ -71,10 +73,48 @@
private ByteBuffer minTerm;
private ByteBuffer maxTerm;
+ final AbstractType<?> termComparator;
long totalBytesAllocated;
int rowCount = 0;
int maxSegmentRowId = -1;
+ public static class KDTreeSegmentBuilder extends SegmentBuilder
+ {
+ protected final byte[] buffer;
+ private final BKDTreeRamBuffer kdTreeRamBuffer;
+
+ public KDTreeSegmentBuilder(AbstractType<?> termComparator, NamedMemoryLimiter limiter)
+ {
+ super(termComparator, limiter);
+
+ int typeSize = TypeUtil.fixedSizeOf(termComparator);
+ this.kdTreeRamBuffer = new BKDTreeRamBuffer(typeSize);
+ this.buffer = new byte[typeSize];
+ this.totalBytesAllocated = this.kdTreeRamBuffer.ramBytesUsed();
+ }
+
+ public boolean isEmpty()
+ {
+ return kdTreeRamBuffer.numRows() == 0;
+ }
+
+ protected long addInternal(ByteBuffer term, int segmentRowId)
Review Comment:
Nit: add `@Override`
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/OneDimBKDPostingsWriter.java:
##########
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bkd tree nodes. If a node has a posting list attached, it will contain every row
+ * id
+ * from all leaves reachable from that node.
+ *
+ * Writer is stateful, because it needs to collect data from bkd index data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ *
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BKDWriter}). That allows us to skip reading the leaves, instead just order leaf
+ * blocks by their offset in the index file, and correlate them with buffered posting lists. We can't make this
+ * assumption for multi-dim case.
+ */
+public class OneDimBKDPostingsWriter implements TraversingBKDReader.IndexTreeTraversalCallback
Review Comment:
The package is named kdtree, most of the classes are named BKD*, and this class is named OneDimBKD*. However, I think everything is about a single unidimensional bkd tree implementation. Is that right? If it is, probably we should use the same naming for the package and all the classes. OneDimBKD* seems the clearest name, but I wouldn't oppose to BKD* or even KD* for brevity.
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java:
##########
@@ -71,10 +73,48 @@
private ByteBuffer minTerm;
private ByteBuffer maxTerm;
+ final AbstractType<?> termComparator;
long totalBytesAllocated;
int rowCount = 0;
int maxSegmentRowId = -1;
+ public static class KDTreeSegmentBuilder extends SegmentBuilder
+ {
+ protected final byte[] buffer;
+ private final BKDTreeRamBuffer kdTreeRamBuffer;
+
+ public KDTreeSegmentBuilder(AbstractType<?> termComparator, NamedMemoryLimiter limiter)
+ {
+ super(termComparator, limiter);
+
+ int typeSize = TypeUtil.fixedSizeOf(termComparator);
+ this.kdTreeRamBuffer = new BKDTreeRamBuffer(typeSize);
+ this.buffer = new byte[typeSize];
+ this.totalBytesAllocated = this.kdTreeRamBuffer.ramBytesUsed();
+ }
+
+ public boolean isEmpty()
Review Comment:
Nit: add `@Override`
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/ImmutableOneDimPointValues.java:
##########
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.index.sai.utils.TermsIterator;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.bkd.BKDWriter;
+
+/**
+ * {@link MutablePointValues} that prevents buffered points from reordering, and always skips sorting phase in Lucene
Review Comment:
```suggestion
* {@link MutableOneDimPointValues} that prevents buffered points from reordering, and always skips sorting phase in Lucene
```
##########
src/java/org/apache/cassandra/index/sai/disk/v1/SSTableIndexWriter.java:
##########
@@ -327,7 +327,9 @@ private void writeSegmentsMetadata() throws IOException
private SegmentBuilder newSegmentBuilder()
{
- SegmentBuilder builder = new SegmentBuilder.RAMStringSegmentBuilder(indexContext.getValidator(), limiter);
+ SegmentBuilder builder = TypeUtil.isLiteral(indexContext.getValidator())
Review Comment:
Not directly related to the changes, but I think that the method `TypeUtil.isUTF8OrAscii` invoked by `TypeUtil.isLiteral` could use `type instanceof StringType` instead of `type instanceof StringType || type instanceof AsciiType`.
##########
src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java:
##########
@@ -31,6 +31,12 @@
*/
META("Meta"),
+ /**
+ * KDTree written by {@code BKDWriter} indexes mappings of term to one or more segment row IDs
+ * (segment row ID = SSTable row ID - segment row ID offset).
+ */
+ KD_TREE("KDTree"),
Review Comment:
Should it be `BKDTree`?
##########
src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java:
##########
@@ -130,6 +131,23 @@ public static ByteComparable max(ByteComparable a, ByteComparable b)
return a == null ? b : (b == null || ByteComparable.compare(b, a, ByteComparable.Version.OSS50) < 0) ? a : b;
}
+ /**
+ * Returns the value length for the given {@link AbstractType}, selecting 16 for types
+ * that officially use VARIABLE_LENGTH but are, in fact, of a fixed length.
+ */
+ public static int fixedSizeOf(AbstractType<?> type)
Review Comment:
Not directly related to this ticket, but it seems to me that the methods on `TypeUtil` do a lot of repeated work in the hot path just to determine the data type of the caller. These operations include finding the base type of reversed multiple times, multiple `instanceof` calls, etc. All these operations on each index are called with the same `IndexContext#getValidator` argument, needlessly repeating work for every column value.
I understand that most of the methods on `TypeUtil` are things that would normally be part of `AbstractType`, so each particular data type can provide its own implementation. But we don't want to couple the generic data types with SAI, so we have this class instead.
I think that we could add a kind of `TermType` class decorating `AbstractType`, and subclass it for every data type that gets special treatment (`LONG`, `VARINT`, `DECIMAL` and `INET`). Then, `IndexContext` would hold the adequate instance of `TermType`. This instance would be able to provide the same operations as the current `TypeUtil`, but without the type checks.
If that makes sense, we could do it on a separate ticket.
##########
src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java:
##########
@@ -186,9 +204,38 @@ public static ByteSource asComparableBytes(ByteBuffer value, AbstractType<?> typ
{
if (type instanceof InetAddressType || type instanceof IntegerType || type instanceof DecimalType)
return ByteSource.optionalFixedLength(ByteBufferAccessor.instance, value);
+ // The LongType.asComparableBytes uses variableLengthInteger which doesn't play well with
+ // the KDTree because it is expecting fixed length data. So for SAI we use a optionalSignedFixedLengthNumber
+ // to keep all comparable values the same length
+ else if (type instanceof LongType)
+ return ByteSource.optionalSignedFixedLengthNumber(ByteBufferAccessor.instance, value);
return type.asComparableBytes(value, version);
}
+ /**
+ * Fills a byte array with the comparable bytes for a type.
+ * <p>
+ * This method expects a {@code value} parameter generated by calling {@link #asIndexBytes(ByteBuffer, AbstractType)}.
+ * It is not generally safe to pass the output of other serialization methods to this method. For instance, it is
+ * not generally safe to pass the output of {@link AbstractType#decompose(Object)} as the {@code value} parameter
+ * (there are certain types for which this is technically OK, but that doesn't hold for all types).
+ *
+ * @param value a value buffer returned by {@link #asIndexBytes(ByteBuffer, AbstractType)}
+ * @param type the type associated with the encoded {@code value} parameter
+ * @param bytes this method's output
+ */
+ public static void toComparableBytes(ByteBuffer value, AbstractType<?> type, byte[] bytes)
+ {
+ if (isInetAddress(type))
+ ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, 16);
+ else if (isBigInteger(type))
+ ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, 20);
+ else if (type instanceof DecimalType)
Review Comment:
I think this can use `isBigDecimal(type)`.
##########
src/java/org/apache/cassandra/index/sai/disk/v1/PerColumnIndexFiles.java:
##########
@@ -50,6 +50,11 @@ public PerColumnIndexFiles(IndexDescriptor indexDescriptor, IndexContext indexCo
files.put(IndexComponent.POSTING_LISTS, indexDescriptor.createPerIndexFileHandle(IndexComponent.POSTING_LISTS, indexContext));
files.put(IndexComponent.TERMS_DATA, indexDescriptor.createPerIndexFileHandle(IndexComponent.TERMS_DATA, indexContext));
}
+ else
+ {
+ files.put(IndexComponent.KD_TREE, indexDescriptor.createPerIndexFileHandle(IndexComponent.KD_TREE, indexContext));
Review Comment:
Nit: The condition of this if block could be `indexContext.isLiteral()` instead of `TypeUtil.isLiteral(indexContext.getValidator())`.
##########
src/java/org/apache/cassandra/index/sai/disk/io/RAMIndexOutput.java:
##########
@@ -70,11 +75,16 @@ public void writeTo(IndexOutput externalOut) throws IOException
externalOut.writeBytes(out.getBytes(), 0, out.getPosition());
}
- public BytesRef getBytes()
+ public BytesRef getBytesRef()
Review Comment:
Nit: I would place getBytes and getBytesRef together, so similar methods stay close to each other.
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java:
##########
@@ -71,10 +73,48 @@
private ByteBuffer minTerm;
private ByteBuffer maxTerm;
+ final AbstractType<?> termComparator;
long totalBytesAllocated;
int rowCount = 0;
int maxSegmentRowId = -1;
+ public static class KDTreeSegmentBuilder extends SegmentBuilder
+ {
+ protected final byte[] buffer;
+ private final BKDTreeRamBuffer kdTreeRamBuffer;
+
+ public KDTreeSegmentBuilder(AbstractType<?> termComparator, NamedMemoryLimiter limiter)
+ {
+ super(termComparator, limiter);
+
+ int typeSize = TypeUtil.fixedSizeOf(termComparator);
+ this.kdTreeRamBuffer = new BKDTreeRamBuffer(typeSize);
+ this.buffer = new byte[typeSize];
+ this.totalBytesAllocated = this.kdTreeRamBuffer.ramBytesUsed();
Review Comment:
Nit: we don't need the `this.`:
```suggestion
kdTreeRamBuffer = new BKDTreeRamBuffer(typeSize);
buffer = new byte[typeSize];
totalBytesAllocated = kdTreeRamBuffer.ramBytesUsed();
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228972457
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class OneDimensionBKDWriter
+ {
+ private final IndexOutput out;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafDocs = new long[maxPointsInLeafNode];
Review Comment:
```suggestion
private final long[] leafRowIDs = new long[maxPointsInLeafNode];
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229338957
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.stream.IntStream;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link IntersectingPointValues}.
+ */
+public class BlockBalancedTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder rowIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BlockBalancedTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+
+ rowIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(rowIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ assert value.length == packedValue.length : "The value has length=" + value.length + " but should be " + packedValue.length;
+
+ long startingBytesUsed = bytesUsed.get();
+ long startingRowIDsBytesUsed = rowIDsBuilder.ramBytesUsed();
+
+ rowIDsBuilder.add(segmentRowId);
+ bytes.append(value);
+
+ if (segmentRowId != lastSegmentRowID)
Review Comment:
I've checked this back to its root caller and the row ID is incremented after each call to add a term to an index so this can just be an assertion.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230020764
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
+ {
+ long balancedTreePosition;
+ SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap();
+
+ LeafCallback leafCallback = new LeafCallback();
+
+ try (IndexOutput balancedTreeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexContext, true))
+ {
+ // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer.
+ long balancedTreeOffset = balancedTreeOutput.getFilePointer();
+
+ balancedTreePosition = writer.writeField(balancedTreeOutput, values, leafCallback);
+
+ // If the balancedTreePosition is less than 0 then we didn't write any values out
+ // and the index is empty
+ if (balancedTreePosition < 0)
+ return components;
+
+ long balancedTreeLength = balancedTreeOutput.getFilePointer() - balancedTreeOffset;
+
+ Map<String, String> attributes = new LinkedHashMap<>();
Review Comment:
nit: Also applies to the attributes for postings below, but do we actually ever do anything with these attributes? They're cheap, but just wondering if we have even a speculative use for them...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228530187
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
Review Comment:
I love that `sort()` here immediately subtracts 1 from `maxDoc`, and that's the only reason we can't just pass the max row ID, but have to first add one to it :D
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228528948
##########
src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java:
##########
@@ -110,7 +112,7 @@ public void complete(Stopwatch stopwatch) throws IOException
try (MemtableTermsIterator terms = new MemtableTermsIterator(memtable.getMinTerm(), memtable.getMaxTerm(), iterator))
{
- long cellCount = flush(minKey, maxKey, indexContext.getValidator(), terms);
+ long cellCount = flush(minKey, maxKey, indexContext.getValidator(), terms, rowMapping.maxSSTableRowId);
Review Comment:
Just to clarify, we pass `maxSSTableRowId` here, but the argument we accept is `maxSegmentRowId`, because we're writing a single segment anyway?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#issuecomment-1589132488
@adelapena @maedhroz I have taken a first stab at changing the naming for the kdtree package and classes. I have only changed the names in the source atm. If we are happy with this then I'll change the tests to match. I have done a first pass through the comments to try and make them better reflect what is going on.
Would you mind having a look at these changes to see if they match your expectations? My main concern is the `bbtree` package name. My only consolation is that is no more or less descriptive than any of the other package names we have.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232811631
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final class PackedIndexTree
+ {
+ private final byte[][] splitPackedValueStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ // used to read the packed byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the per-dim delta we read for the node at this level is a negative offset vs. the last split on this dim; this is a packed
+ // 2D array, i.e. to access array[level][dim] you read from negativeDeltas[level*numDims+dim]. this will be true if the last time we
+ // split on this dimension, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+ // scratch value to return from getPackedValue:
+ private final BytesRef scratch;
+
+ PackedIndexTree()
+ {
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[packedBytesLength];
+ readNodeData(false);
+ scratch = new BytesRef();
+ scratch.length = bytesPerValue;
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = true;
+ in.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = false;
+ in.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ leafNode = false;
+ }
+
+ public boolean isLeafNode()
+ {
+ return nodeID >= numLeaves;
+ }
+
+ public boolean nodeExists()
+ {
+ return nodeID - numLeaves < numLeaves;
+ }
Review Comment:
nit: I'm 50/50 on whether it makes sense for `isLeafNode()` and `nodeExists()` to remind the reader via JavaDoc that the calculations work because the tree is balanced :)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232829084
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BlockBalancedTreeWriter}). That allows us to skip reading the leaves, instead
+ * just order leaf blocks by their offset in the index file, and correlate them with buffered posting lists.
+ */
+public class BlockBalancedTreePostingsWriter implements TraversingBlockBalancedTreeReader.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
Review Comment:
nit: `leafPostings` ?
That might not be much better, but I'll throw it out there anyway...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1234541837
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
Review Comment:
nit: It might also be nicer to just make `postingLists` a field on the `Intersection` so we don't have to pass it through so many methods below. This class is basically a "collector"...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228536333
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
Review Comment:
```suggestion
checkArgument(maxSegmentRowId >= 0, "[%s] maxSegmentRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
```
?
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
Review Comment:
```suggestion
.add("bytesPerValue", bytesPerValue)
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228539987
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
Review Comment:
nit: This is only valid after the writer is used to write points?
In general, I'm not sure what we want the `toString()` to do here. Maybe take out the point count and throw in the `IndexContext`, which has a well-defined `toString()` itself?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230121390
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
Review Comment:
There's the temptation to get format changes in before our first release, but I know we've got to draw the line somewhere. It's probably okay for us to have a short-term focus, given the timeline for 5.0, and punt that to phase 2, as long as that Jira clearly describes what we might want to do so it isn't super foreign when we do pick it up.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230025604
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BlockBalancedTreeWriter}). That allows us to skip reading the leaves, instead
+ * just order leaf blocks by their offset in the index file, and correlate them with buffered posting lists.
+ */
+public class BlockBalancedTreePostingsWriter implements TraversingBlockBalancedTreeReader.IndexTreeTraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
+ private final TreeMap<Long, Integer> leafOffsetToNodeID = new TreeMap<>(Long::compareTo);
+ private final Multimap<Integer, Integer> nodeToChildLeaves = HashMultimap.create();
+
+ private final IndexContext indexContext;
+ int numNonLeafPostings = 0;
+ int numLeafPostings = 0;
+
+ BlockBalancedTreePostingsWriter(List<PackedLongValues> postings, IndexContext indexContext)
+ {
+ this.postings = postings;
+ this.indexContext = indexContext;
+ }
+
+ @Override
+ public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot)
+ {
+ checkArgument(!pathToRoot.containsInt(leafNodeID));
+ checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1));
+
+ leafOffsetToNodeID.put(leafBlockFP, leafNodeID);
+ for (int i = 0; i < pathToRoot.size(); i++)
+ {
+ int level = i + 1;
+ if (isLevelEligibleForPostingList(level))
+ {
+ int nodeID = pathToRoot.get(i);
+ nodeToChildLeaves.put(nodeID, leafNodeID);
+ }
+ }
+ }
+
+ public long finish(IndexOutput out) throws IOException
Review Comment:
Feels like a JavaDoc on the method would be helpful, if only to be clear about what the returned position actually is. (It looks like the position in the postings file where the node ID -> posting list map is written, rather than the footer?)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228571704
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
Review Comment:
nit: I guess we could simplify this bit of documentation even more, eliminate the "1-dim point" stuff.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230131645
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
Review Comment:
> One option here would be to change how we build the in-memory numerics such that they are naturally sorted. I am thinking of a trie here.
I like this, and I like it a lot. I'm +100 on just replacing `BlockBalancedTreeRamBuffer` entirely w/ an in-memory trie and blowing away all the `PointValues` garbage. Then we end up having something more like `ImmutableIntersectingPointValues` for all segment building cases?
What do we lose if we toss out `BlockBalancedTreeRamBuffer`? `PackedLongValues` packing row IDs more efficiently than the trie stores them as payloads? If losing that and gaining value prefix compression was about break-even in terms of heap used, being able to avoid sorting would still be a win...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#issuecomment-1591456461
@adelapena @maedhroz I have pushed another commit with more renaming, tidying and javadoc. I have addressed the majority of points raised above but still have some work to do in the `BlockBalancedTreeWriter` and associated classes. I will continue with this tomorrow.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1235733022
##########
src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java:
##########
@@ -31,6 +31,12 @@
*/
META("Meta"),
+ /**
+ * Balanced tree written by {@code BlockBalancedTreeWriter} indexes mappings of term to one or more segment row IDs
+ * (segment row ID = SSTable row ID - segment row ID offset).
+ */
+ BALANCED_TREE("BalancedTree"),
Review Comment:
Do we need to update `V1OnDiskFormat#perColumnIndexComponents()` to include this? (i.e. Return this and postings if we've got a numeric column index?) Feels like this should also be failing a test, but isn't currently because `StorageAttachedIndexDDLTest#verifyRebuildCorruptedFiles()` (for example) is only lookin at literal column indexes...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232958983
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
Review Comment:
So to confirm my understanding, the bitset here represents the indexes of the leaf postings. At least from the usage in `FilteringPostingList`, it seems that way...
```
int position = 0;
...
if (filter.get(position++))
{
return segmentRowId;
}
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#issuecomment-1604167364
@maedhroz @adelapena I have pushed an intermediate commit just to get things off of my laptop. This contains a number of renaming and javadoc improvements but doesn't complete the review changes.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239696921
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BlockBalancedTreeWriter}). That allows us to skip reading the leaves, instead
+ * just order leaf blocks by their offset in the index file, and correlate them with buffered posting lists.
+ */
+public class BlockBalancedTreePostingsWriter implements TraversingBlockBalancedTreeReader.IndexTreeTraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
+ private final TreeMap<Long, Integer> leafOffsetToNodeID = new TreeMap<>(Long::compareTo);
+ private final Multimap<Integer, Integer> nodeToChildLeaves = HashMultimap.create();
+
+ private final IndexContext indexContext;
+ int numNonLeafPostings = 0;
+ int numLeafPostings = 0;
+
+ BlockBalancedTreePostingsWriter(List<PackedLongValues> postings, IndexContext indexContext)
+ {
+ this.postings = postings;
+ this.indexContext = indexContext;
+ }
+
+ @Override
+ public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot)
+ {
+ checkArgument(!pathToRoot.containsInt(leafNodeID));
+ checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1));
+
+ leafOffsetToNodeID.put(leafBlockFP, leafNodeID);
+ for (int i = 0; i < pathToRoot.size(); i++)
+ {
+ int level = i + 1;
+ if (isLevelEligibleForPostingList(level))
+ {
+ int nodeID = pathToRoot.get(i);
+ nodeToChildLeaves.put(nodeID, leafNodeID);
+ }
+ }
+ }
+
+ public long finish(IndexOutput out) throws IOException
Review Comment:
I have done this and have noted a few optimisations that can be made to this process.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242150441
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
Review Comment:
I think these should be `CorruptIndexException`. It is the only reason we are going to get this.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245751023
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Specialized writer for point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ * <p>
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+ private static final int DEFAULT_POSTINGS_SIZE = 128;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxRows` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxSegmentRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("indexContext", indexContext)
+ .add("bytesPerValue", bytesPerValue)
+ .toString();
+ }
+
+ private static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> leafPostings = new ArrayList<>(DEFAULT_POSTINGS_SIZE);
+
+ public int numLeaves()
+ {
+ return leafPostings.size();
+ }
+
+ @Override
+ public void writeLeafPostings(BlockBalancedTreeWriter.RowIDAndIndex[] leafPostings, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(leafPostings[i].rowID);
+ }
+ this.leafPostings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link IntersectingPointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeCompleteSegment(IntersectingPointValues values) throws IOException
+ {
+ long treePosition;
+
+ SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap();
+
+ LeafCallback leafCallback = new LeafCallback();
+
+ try (IndexOutput treeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexContext, true))
+ {
+ // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer.
+ long treeOffset = treeOutput.getFilePointer();
+
+ treePosition = writer.write(treeOutput, values, leafCallback);
+
+ // If the treePosition is less than 0 then we didn't write any values out and the index is empty
+ if (treePosition < 0)
+ return components;
+
+ long treeLength = treeOutput.getFilePointer() - treeOffset;
+
+ Map<String, String> attributes = new LinkedHashMap<>();
+ attributes.put("max_points_in_leaf_node", Integer.toString(writer.getMaxPointsInLeafNode()));
+ attributes.put("num_leaves", Integer.toString(leafCallback.numLeaves()));
+ attributes.put("num_points", Long.toString(writer.getValueCount()));
+ attributes.put("bytes_per_value", Long.toString(writer.getBytesPerValue()));
+
+ components.put(IndexComponent.BALANCED_TREE, treePosition, treeOffset, treeLength, attributes);
+ }
+
+ try (BlockBalancedTreeWalker reader = new BlockBalancedTreeWalker(indexDescriptor.createPerIndexFileHandle(IndexComponent.BALANCED_TREE, indexContext), treePosition);
+ IndexOutput postingsOutput = indexDescriptor.openPerIndexOutput(IndexComponent.POSTING_LISTS, indexContext, true))
+ {
+ long postingsOffset = postingsOutput.getFilePointer();
+
+ BlockBalancedTreePostingsWriter postingsWriter = new BlockBalancedTreePostingsWriter();
+ reader.traverse(postingsWriter);
+
+ // The balanced tree postings writer already writes its own header & footer.
+ long postingsPosition = postingsWriter.finish(postingsOutput, leafCallback.leafPostings, indexContext);
+
+ Map<String, String> attributes = new LinkedHashMap<>();
+ attributes.put("num_leaf_postings", Integer.toString(postingsWriter.numLeafPostings));
+ attributes.put("num_non_leaf_postings", Integer.toString(postingsWriter.numNonLeafPostings));
+
+ long postingsLength = postingsOutput.getFilePointer() - postingsOffset;
+ components.put(IndexComponent.POSTING_LISTS, postingsPosition, postingsOffset, postingsLength, attributes);
+ }
+
+ return components;
+ }
+
+ /**
+ * @return number of points added
+ */
+ public long getPointCount()
Review Comment:
```suggestion
public long getValueCount()
```
?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245769303
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+ private final long maxRows;
+
+ public BlockBalancedTreeWriter(long maxRows, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxRows = maxRows;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxRows), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> leafWriter.add(packedValue, rowID));
+
+ valueCount = leafWriter.finish();
+
+ long treeFilePointer = valueCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ // There is only any point in writing the balanced tree if any values were added
+ if (treeFilePointer >= 0)
+ writeBalancedTree(treeOutput, maxPointsInLeafNode, leafWriter.leafBlockStartValues, leafWriter.leafBlockFilePointers);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return treeFilePointer;
+ }
+
+ private void writeBalancedTree(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitValues = new byte[(1 + numInnerNodes) * bytesPerValue];
+ int depth = recurseBalanceTree(1, 0, numInnerNodes, 1, splitValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ out.writeVInt(leafBlockFPs.length);
+ out.writeVInt(Math.min(depth, leafBlockFPs.length));
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(valueCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ /**
+ * This can, potentially, be removed in the future by CASSANDRA-18597
+ */
+ private int recurseBalanceTree(int nodeID, int offset, int count, int treeDepth, byte[] splitValues, List<byte[]> leafBlockStartValues)
+ {
+ treeDepth++;
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ int leftTreeDepth = recurseBalanceTree(2 * nodeID, offset, leftHalf, treeDepth, splitValues, leafBlockStartValues);
+
+ // Recurse right
+ int rightTreeDepth = recurseBalanceTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, treeDepth, splitValues, leafBlockStartValues);
+ return Math.max(leftTreeDepth, rightTreeDepth);
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
Review Comment:
Is it illegal for us to get here now?
It seems like if you had `count == 0` the returned depth would be 2?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247635387
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.ResettableByteBuffersIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.Pair;
+import org.apache.lucene.store.ByteBuffersDataOutput;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+
+import static org.apache.cassandra.index.sai.postings.PostingList.END_OF_STREAM;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+
+ public BlockBalancedTreeWriter(int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the sorted values from a {@link BlockBalancedTreeIterator}.
+ * <p>
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param values The {@link BlockBalancedTreeIterator} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, BlockBalancedTreeIterator values, final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ while (values.hasNext())
+ {
+ Pair<byte[], PostingList> pair = values.next();
+ while (true)
+ {
+ long segmentRowId = pair.right.nextPosting();
+ if (segmentRowId == END_OF_STREAM)
+ break;
+ leafWriter.add(pair.left, segmentRowId);
+ }
Review Comment:
We should definitely avoid boxing the values but I have changed the loop.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227229654
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/OneDimBKDPostingsWriter.java:
##########
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bkd tree nodes. If a node has a posting list attached, it will contain every row
+ * id
+ * from all leaves reachable from that node.
+ *
+ * Writer is stateful, because it needs to collect data from bkd index data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ *
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BKDWriter}). That allows us to skip reading the leaves, instead just order leaf
+ * blocks by their offset in the index file, and correlate them with buffered posting lists. We can't make this
+ * assumption for multi-dim case.
+ */
+public class OneDimBKDPostingsWriter implements TraversingBKDReader.IndexTreeTraversalCallback
Review Comment:
https://github.com/apache/cassandra/pull/2409/files#r1227216477
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228828531
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
Review Comment:
nit: Looking for small optimizations here. Is there any way we could estimate the number of leaves when we create this list rather than potentially having to grow a few times? I guess this is at the per-segment level, so not super important.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232441876
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
Review Comment:
```suggestion
private final List<Long> leafBlockFilePointers = new ArrayList<>();
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232559221
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(treeOutput.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
+ int commonPrefixLength = bytesPerValue;
+ int offset = (leafValueCount - 1) * bytesPerValue;
+ for (int j = 0; j < bytesPerValue; j++)
+ {
+ if (leafValues[j] != leafValues[offset + j])
+ {
+ commonPrefixLength = j;
+ break;
+ }
+ }
+
+ assert scratchOut.getPosition() == 0;
+
+ treeOutput.writeVInt(leafValueCount);
+
+ for (int x = 0; x < leafValueCount; x++)
+ {
+ rowIDAndIndexes[x].valueOrderIndex = x;
+ rowIDAndIndexes[x].rowID = leafRowIDs[x];
+ }
+
+ final Sorter sorter = new IntroSorter()
+ {
+ RowIDAndIndex pivot;
+
+ @Override
+ protected void swap(int i, int j)
+ {
+ RowIDAndIndex o = rowIDAndIndexes[i];
+ rowIDAndIndexes[i] = rowIDAndIndexes[j];
+ rowIDAndIndexes[j] = o;
+ }
+
+ @Override
+ protected void setPivot(int i)
+ {
+ pivot = rowIDAndIndexes[i];
+ }
+
+ @Override
+ protected int comparePivot(int j)
+ {
+ return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID);
+ }
+ };
+
+ sorter.sort(0, leafValueCount);
+
+ // write leaf rowID -> orig index
+ scratchOut2.reset();
+
+ // iterate in row ID order to get the row ID index for the given value order index
+ // place into an array to be written as packed ints
+ for (int x = 0; x < leafValueCount; x++)
+ orderIndex[rowIDAndIndexes[x].valueOrderIndex] = x;
+
+ LeafOrderMap.write(orderIndex, leafValueCount, maxPointsInLeafNode - 1, scratchOut2);
+
+ treeOutput.writeVInt(scratchOut2.getPosition());
+ treeOutput.writeBytes(scratchOut2.getBytes(), 0, scratchOut2.getPosition());
+
+ callback.writeLeafDocs(rowIDAndIndexes, 0, leafValueCount);
+
+ writeCommonPrefix(scratchOut, commonPrefixLength, leafValues);
+
+ scratchBytesRef1.length = bytesPerValue;
+ scratchBytesRef1.bytes = leafValues;
+
+ IntFunction<BytesRef> packedValues = (i) -> {
+ scratchBytesRef1.offset = bytesPerValue * i;
+ return scratchBytesRef1;
+ };
+ if (DEBUG)
+ valuesInOrderAndBounds(leafValueCount,
+ ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue),
+ ArrayUtil.copyOfSubArray(leafValues, (leafValueCount - 1) * bytesPerValue, leafValueCount * bytesPerValue),
+ packedValues,
+ leafRowIDs);
+
+ writeLeafBlockPackedValues(scratchOut, commonPrefixLength, leafValueCount, packedValues);
+
+ treeOutput.writeBytes(scratchOut.getBytes(), 0, scratchOut.getPosition());
+
+ scratchOut.reset();
+ }
+
+ private void checkMaxLeafNodeCount(int numLeaves)
+ {
+ if ((1 + bytesPerValue) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH)
Review Comment:
Where does the `1 + ` come from?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230773618
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
Review Comment:
I think we need to boost the testing on this. I found the same thing when I was doing the initial analysis of the internals and had to create an extended test to hit some of these code paths. Now I have a better understanding of these internals I'll try and put something together that correctly hits and tests these paths.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247626622
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.db.memtable.TrieMemtable;
+import org.apache.cassandra.db.tries.InMemoryTrie;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.bytecomparable.ByteSource;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for values that provides a sorted view of itself as a {@link BlockBalancedTreeIterator}.
+ */
+@NotThreadSafe
+public class BlockBalancedTreeRamBuffer
+{
+ private final InMemoryTrie<PackedLongValues.Builder> trieBuffer;
+ private final RowIDReducer reducer;
+ private final int bytesPerValue;
+ private int numRows;
+
+ public BlockBalancedTreeRamBuffer(int bytesPerValue)
+ {
+ trieBuffer = new InMemoryTrie<>(TrieMemtable.BUFFER_TYPE);
+ reducer = new RowIDReducer();
+ this.bytesPerValue = bytesPerValue;
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long memoryUsed()
+ {
+ return trieBuffer.sizeOnHeap() + reducer.heapAllocations();
+ }
+
+ public long add(int segmentRowId, byte[] value)
+ {
+ final long initialSizeOnHeap = trieBuffer.sizeOnHeap();
+ final long reducerHeapSize = reducer.heapAllocations();
+
+ try
+ {
+ trieBuffer.putRecursive(v -> ByteSource.fixedLength(value), segmentRowId, reducer);
+ }
+ catch (InMemoryTrie.SpaceExhaustedException e)
+ {
+ throw Throwables.unchecked(e);
+ }
+
+ numRows++;
+ return (trieBuffer.sizeOnHeap() - initialSizeOnHeap) + (reducer.heapAllocations() - reducerHeapSize);
+ }
+
+ public BlockBalancedTreeIterator iterator()
+ {
+ return BlockBalancedTreeIterator.fromTrieIterator(trieBuffer.entrySet().iterator(), bytesPerValue);
+ }
+
+ private static class RowIDReducer implements InMemoryTrie.UpsertTransformer<PackedLongValues.Builder, Integer>
+ {
+ private final LongAdder heapAllocations = new LongAdder();
+
+ @Override
+ public PackedLongValues.Builder apply(PackedLongValues.Builder existing, Integer neww)
Review Comment:
TBH, it should be called `rowID` because that's what it is. I've called it that.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227234214
##########
src/java/org/apache/cassandra/config/CassandraRelevantProperties.java:
##########
@@ -405,6 +405,16 @@
SCHEMA_UPDATE_HANDLER_FACTORY_CLASS("cassandra.schema.update_handler_factory.class"),
SEARCH_CONCURRENCY_FACTOR("cassandra.search_concurrency_factor", "1"),
+ /** Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list */
+ SAI_MINIMUM_POSTINGS_LEAVES("cassandra.sai.minimum_postings_leaves", "64"),
+
+ /**
+ * Skip, or the sampling interval, for selecting a bkd tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but bkd tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ SAI_POSTINGS_SKIP("cassandra.sai.postings_skip", "3"),
Review Comment:
nit: I think we need to move these up after `SAI_MAX_STRING_TERM_SIZE` so we maintain order
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228830677
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class OneDimensionBKDWriter
+ {
+ private final IndexOutput out;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafDocs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafCount;
+ private long lastDocID;
+
+ OneDimensionBKDWriter(IndexOutput out, Callback callback)
+ {
+ this.out = out;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafCount, lastPackedValue, packedValue, 0, docID, lastDocID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafCount * bytesPerValue, bytesPerValue);
+ leafDocs[leafCount] = docID;
+ 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();
+ leafCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastDocID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ public long finish() throws IOException
+ {
+ if (leafCount > 0)
+ {
+ writeLeafBlock();
+ leafCount = 0;
+ }
+
+ if (valueCount == 0)
+ {
+ return -1;
+ }
+
+ pointCount = valueCount;
+
+ long indexFP = out.getFilePointer();
+
+ int numInnerNodes = leafBlockStartValues.size();
+
+ byte[] index = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, index, leafBlockStartValues);
+ long[] arr = new long[leafBlockFilePointer.size()];
+ for (int i = 0; i < leafBlockFilePointer.size(); i++)
+ {
+ arr[i] = leafBlockFilePointer.get(i);
+ }
+ writeIndex(out, maxPointsInLeafNode, arr, index);
+ return indexFP;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(out.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
+ int prefix = bytesPerValue;
+ int offset = (leafCount - 1) * bytesPerValue;
+ for (int j = 0; j < bytesPerValue; j++)
+ {
+ if (leafValues[j] != leafValues[offset + j])
+ {
+ prefix = j;
+ break;
+ }
+ }
+
+ int commonPrefixLength = prefix;
+
+ assert scratchOut.getPosition() == 0;
+
+ out.writeVInt(leafCount);
+
+ for (int x = 0; x < leafCount; x++)
+ {
+ rowIDAndIndexes[x].valueOrderIndex = x;
+ rowIDAndIndexes[x].rowID = leafDocs[x];
+ }
+
+ final Sorter sorter = new IntroSorter()
+ {
+ RowIDAndIndex pivot;
+
+ @Override
+ protected void swap(int i, int j)
+ {
+ RowIDAndIndex o = rowIDAndIndexes[i];
+ rowIDAndIndexes[i] = rowIDAndIndexes[j];
+ rowIDAndIndexes[j] = o;
+ }
+
+ @Override
+ protected void setPivot(int i)
+ {
+ pivot = rowIDAndIndexes[i];
+ }
+
+ @Override
+ protected int comparePivot(int j)
+ {
+ return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID);
+ }
+ };
+
+ sorter.sort(0, leafCount);
+
+ // write leaf rowID -> orig index
+ scratchOut2.reset();
+
+ // iterate in row ID order to get the row ID index for the given value order index
+ // place into an array to be written as packed ints
+ for (int x = 0; x < leafCount; x++)
+ {
+ final int valueOrderIndex = rowIDAndIndexes[x].valueOrderIndex;
+ orderIndex[valueOrderIndex] = x;
+ }
+
+ LeafOrderMap.write(orderIndex, leafCount, maxPointsInLeafNode - 1, scratchOut2);
+
+ out.writeVInt(scratchOut2.getPosition());
+ out.writeBytes(scratchOut2.getBytes(), 0, scratchOut2.getPosition());
+
+ if (callback != null) callback.writeLeafDocs(rowIDAndIndexes, 0, leafCount);
+
+ writeCommonPrefixLength(scratchOut, commonPrefixLength, leafValues);
+
+ scratchBytesRef1.length = bytesPerValue;
+ scratchBytesRef1.bytes = leafValues;
+
+ IntFunction<BytesRef> packedValues = (i) -> {
+ scratchBytesRef1.offset = bytesPerValue * i;
+ return scratchBytesRef1;
+ };
+ if (DEBUG)
+ valuesInOrderAndBounds(leafCount,
+ ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue),
+ ArrayUtil.copyOfSubArray(leafValues, (leafCount - 1) * bytesPerValue, leafCount * bytesPerValue),
+ packedValues,
+ leafDocs);
+
+ writeLeafBlockPackedValues(scratchOut, commonPrefixLength, leafCount, packedValues);
+
+ out.writeBytes(scratchOut.getBytes(), 0, scratchOut.getPosition());
+
+ scratchOut.reset();
+ }
+ }
+
+ // TODO: there must be a simpler way?
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ private void checkMaxLeafNodeCount(int numLeaves)
+ {
+ if ((1 + bytesPerValue) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH)
+ {
+ throw new IllegalStateException("too many nodes; increase maxPointsInLeafNode (currently " + maxPointsInLeafNode + ") and reindex");
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index not fully balanced binary tree (only happens
+ // if it was created by OneDimensionBKDWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+ writeIndex(out, countPerLeaf, leafBlockFPs.length, packedIndex);
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, int numLeaves, byte[] packedIndex) throws IOException
+ {
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert numLeaves > 0;
+ out.writeVInt(numLeaves);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void writeLeafBlockPackedValues(DataOutput out, int commonPrefixLength, int count, IntFunction<BytesRef> packedValues) throws IOException
+ {
+ if (commonPrefixLength == bytesPerValue)
+ {
+ // all values in this block are equal
+ out.writeByte((byte) -1);
+ }
+ else
+ {
+ assert commonPrefixLength < bytesPerValue;
+ out.writeByte((byte) 0);
+ int compressedByteOffset = commonPrefixLength;
+ commonPrefixLength++;
+ 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, commonPrefixLength, i, i + runLen, packedValues);
+ i += runLen;
+ assert i <= count;
+ }
+ }
+ }
+
+
+ private void writeLeafBlockPackedValuesRange(DataOutput out, int commonPrefixLength, int start, int end, IntFunction<BytesRef> packedValues) throws IOException
+ {
+ for (int i = start; i < end; ++i)
+ {
+ BytesRef ref = packedValues.apply(i);
+ assert ref.length == bytesPerValue;
+
+ out.writeBytes(ref.bytes, ref.offset + commonPrefixLength, bytesPerValue - commonPrefixLength);
+ }
+ }
+
+ private static int runLen(IntFunction<BytesRef> packedValues, int start, int end, int byteOffset)
+ {
+ BytesRef first = packedValues.apply(start);
+ byte b = first.bytes[first.offset + byteOffset];
+ for (int i = start + 1; i < end; ++i)
+ {
+ BytesRef ref = packedValues.apply(i);
+ byte b2 = ref.bytes[ref.offset + byteOffset];
+ assert Byte.toUnsignedInt(b2) >= Byte.toUnsignedInt(b);
+ if (b != b2)
+ {
+ return i - start;
+ }
+ }
+ return end - start;
+ }
+
+ private void writeCommonPrefixLength(DataOutput out, int commonPrefixLength, byte[] packedValue) throws IOException
+ {
+ out.writeVInt(commonPrefixLength);
+ if (commonPrefixLength > 0)
+ out.writeBytes(packedValue, 0, commonPrefixLength);
+ }
+
+ // The following 3 methods are only used when DEBUG is true:
+
+ private void valueInBounds(BytesRef packedValue, byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ if (FutureArrays.compareUnsigned(packedValue.bytes,
+ packedValue.offset,
+ packedValue.offset + bytesPerValue,
+ minPackedValue,
+ 0,
+ bytesPerValue) < 0)
+ {
+ throw new AssertionError("value=" + new BytesRef(packedValue.bytes, packedValue.offset, bytesPerValue) +
+ " is < minPackedValue=" + new BytesRef(minPackedValue));
+ }
+
+ if (FutureArrays.compareUnsigned(packedValue.bytes,
+ packedValue.offset,
+ packedValue.offset + bytesPerValue,
+ maxPackedValue, 0,
+ bytesPerValue) > 0)
+ {
+ throw new AssertionError("value=" + new BytesRef(packedValue.bytes, packedValue.offset, bytesPerValue) +
+ " is > maxPackedValue=" + new BytesRef(maxPackedValue));
+ }
+ }
+
+ private void valuesInOrderAndBounds(int count, byte[] minPackedValue, byte[] maxPackedValue, IntFunction<BytesRef> values, long[] docs)
+ {
+ byte[] lastPackedValue = new byte[bytesPerValue];
+ long lastDoc = -1;
+ for (int i = 0; i < count; i++)
+ {
+ BytesRef packedValue = values.apply(i);
+ assert packedValue.length == bytesPerValue;
+ valueInOrder(i, lastPackedValue, packedValue.bytes, packedValue.offset, docs[i], lastDoc);
+ lastDoc = docs[i];
+
+ // Make sure this value does in fact fall within this leaf cell:
+ valueInBounds(packedValue, minPackedValue, maxPackedValue);
+ }
+ }
+
+ private void valueInOrder(long ord, byte[] lastPackedValue, byte[] packedValue, int packedValueOffset, long doc, long lastDoc)
+ {
+ int dimOffset = 0;
+ if (ord > 0)
+ {
+ int cmp = FutureArrays.compareUnsigned(lastPackedValue, dimOffset, dimOffset + bytesPerValue, packedValue, packedValueOffset + dimOffset, packedValueOffset + dimOffset + bytesPerValue);
+ if (cmp > 0)
+ {
+ throw new AssertionError("values out of order: last value=" + new BytesRef(lastPackedValue) +
+ " current value=" + new BytesRef(packedValue, packedValueOffset, bytesPerValue) +
+ " ord=" + ord);
+ }
+ if (cmp == 0 && doc < lastDoc)
+ {
+ throw new AssertionError("docs out of order: last doc=" + lastDoc + " current doc=" + doc + " ord=" + ord);
+ }
+ }
+ System.arraycopy(packedValue, packedValueOffset, lastPackedValue, 0, bytesPerValue);
+ }
+}
Review Comment:
nit: Given `OneDimensionBKDWriter` isn't static, should we just move the methods here into it and make/keep them private if possible? Not sure why some are in there and others aren't...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228458908
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
Review Comment:
```suggestion
* <p>
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228449521
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.stream.IntStream;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link IntersectingPointValues}.
+ */
+public class BlockBalancedTreeRamBuffer implements Accountable
Review Comment:
nit: Could throw a `@NotThreadSafe` on here
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228670379
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
Review Comment:
I'd consider a small set of changes around sorting here:
1.) Remove the `size()` method in the super right here. It's already implemented in the RAM buffer version.
2.) Have `ImmutableIntersectingPointValues` add the following:
```
@Override
public boolean needsSorting()
{
return false;
}
@Override
public long size()
{
throw new UnsupportedOperationException();
}
```
3.) Fix `ImmutableIntersectingPointValuesTest#shouldSkipLuceneSorting()` to have a `needsSorting()` check or to hard code a size of 1.
We don't have any intention of ever sorting the immutable version, so might as well make that explicit (and avoid providing a dummy size that isn't actually a size).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242127270
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsIndex.java:
##########
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import com.carrotsearch.hppc.IntLongHashMap;
+import com.carrotsearch.hppc.IntLongMap;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.RandomAccessReader;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.cassandra.index.sai.disk.v1.SAICodecUtils.validate;
+
+/**
+ * Mapping between node ID and an offset to its auxiliary posting list (containing every row id from all leaves
+ * reachable from that node. See {@link BlockBalancedTreePostingsWriter}).
+ */
+class BlockBalancedTreePostingsIndex
+{
+ private final int size;
+ public final IntLongMap index = new IntLongHashMap();
+
+ BlockBalancedTreePostingsIndex(FileHandle postingsFileHandle, long filePosition) throws IOException
+ {
+ try (RandomAccessReader reader = postingsFileHandle.createReader();
+ IndexInputReader input = IndexInputReader.create(reader))
+ {
+ validate(input);
+ input.seek(filePosition);
+
+ size = input.readVInt();
+
+ for (int x = 0; x < size; x++)
+ {
+ final int node = input.readVInt();
+ final long filePointer = input.readVLong();
+
+ index.put(node, filePointer);
+ }
+ }
+ }
+
+ /**
+ * Returns <tt>true</tt> if given node ID has an auxiliary posting list.
+ */
+ boolean exists(int nodeID)
+ {
+ checkArgument(nodeID > 0);
+ return index.containsKey(nodeID);
+ }
+
+ /**
+ * Returns an offset within the balanced tree postings file to the begining of the blocks summary of given node's auxiliary
+ * posting list.
+ *
+ * @throws IllegalArgumentException when given nodeID doesn't have an auxiliary posting list. Check first with
+ * {@link #exists(int)}
+ */
+ long getPostingsFilePointer(int nodeID)
+ {
+ checkArgument(exists(nodeID));
Review Comment:
Agreed. The usage shows that `getPostingsFilePointer` is always preceded by a call to `exists` so we definitely don't need to check there. Then `exists` is always preceded by a call to `getNodeID` from the traversal state. So, I have pushed the assertion up to there. It makes sense to keep it in the state in one place.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242338638
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
Review Comment:
That is my understanding. This area of the code is overly confusing because we have an `OrdinalPostingList` that returns the position of the posting in the posting list rather than the posting itself and then this is used to check the bitset. If the bitset says ok then we return the row ID matching the position from the posting list.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242361694
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
+ // If the compressed dimension is -1 it means that all values in the block are equal
+ if (compressedDimension == -1)
+ return visitRawDocValues(count, visitor, origIndex);
+ else
+ return visitCompressedDocValues(commonPrefixLength, in, count, visitor, origIndex);
+ }
+
+ private int readCompressedDimension(IndexInput in) throws IOException
+ {
+ int compressedDimension = in.readByte();
+ if (compressedDimension != -1 && compressedDimension != 0)
+ throw new CorruptIndexException(String.format("Dimension should be in the range [-1, 0], but was %d.", compressedDimension), in);
+ return compressedDimension;
+ }
+
+ private FixedBitSet visitCompressedDocValues(int commonPrefixLength,
Review Comment:
I have removed the `Doc` element here so we have `visitValues`, `visitRawValues` and `visitCompressedValues`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1234546699
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
Review Comment:
nit: These checks feel expensive, but I suppose there would be real safety issues if the split value was out of range. I guess we should just make the messages more helpful.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1234552738
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final class PackedIndexTree
Review Comment:
nit: You can kind of start to see the structure in the push methods, in terms of how it addresses children in the array, but it might be helpful to have a little schematic of how the series of node IDs maps to the tree. (ASCII art time? lol)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232780106
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(treeOutput.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
+ int commonPrefixLength = bytesPerValue;
+ int offset = (leafValueCount - 1) * bytesPerValue;
+ for (int j = 0; j < bytesPerValue; j++)
+ {
+ if (leafValues[j] != leafValues[offset + j])
+ {
+ commonPrefixLength = j;
+ break;
+ }
+ }
+
+ assert scratchOut.getPosition() == 0;
+
+ treeOutput.writeVInt(leafValueCount);
+
+ for (int x = 0; x < leafValueCount; x++)
+ {
+ rowIDAndIndexes[x].valueOrderIndex = x;
+ rowIDAndIndexes[x].rowID = leafRowIDs[x];
+ }
+
+ final Sorter sorter = new IntroSorter()
+ {
+ RowIDAndIndex pivot;
+
+ @Override
+ protected void swap(int i, int j)
+ {
+ RowIDAndIndex o = rowIDAndIndexes[i];
+ rowIDAndIndexes[i] = rowIDAndIndexes[j];
+ rowIDAndIndexes[j] = o;
+ }
+
+ @Override
+ protected void setPivot(int i)
+ {
+ pivot = rowIDAndIndexes[i];
+ }
+
+ @Override
+ protected int comparePivot(int j)
+ {
+ return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID);
+ }
+ };
+
+ sorter.sort(0, leafValueCount);
+
+ // write leaf rowID -> orig index
+ scratchOut2.reset();
+
+ // iterate in row ID order to get the row ID index for the given value order index
+ // place into an array to be written as packed ints
+ for (int x = 0; x < leafValueCount; x++)
+ orderIndex[rowIDAndIndexes[x].valueOrderIndex] = x;
+
+ LeafOrderMap.write(orderIndex, leafValueCount, maxPointsInLeafNode - 1, scratchOut2);
+
+ treeOutput.writeVInt(scratchOut2.getPosition());
+ treeOutput.writeBytes(scratchOut2.getBytes(), 0, scratchOut2.getPosition());
+
+ callback.writeLeafDocs(rowIDAndIndexes, 0, leafValueCount);
+
+ writeCommonPrefix(scratchOut, commonPrefixLength, leafValues);
+
+ scratchBytesRef1.length = bytesPerValue;
+ scratchBytesRef1.bytes = leafValues;
+
+ IntFunction<BytesRef> packedValues = (i) -> {
+ scratchBytesRef1.offset = bytesPerValue * i;
+ return scratchBytesRef1;
+ };
+ if (DEBUG)
+ valuesInOrderAndBounds(leafValueCount,
+ ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue),
+ ArrayUtil.copyOfSubArray(leafValues, (leafValueCount - 1) * bytesPerValue, leafValueCount * bytesPerValue),
+ packedValues,
+ leafRowIDs);
+
+ writeLeafBlockPackedValues(scratchOut, commonPrefixLength, leafValueCount, packedValues);
+
+ treeOutput.writeBytes(scratchOut.getBytes(), 0, scratchOut.getPosition());
+
+ scratchOut.reset();
+ }
+
+ private void checkMaxLeafNodeCount(int numLeaves)
+ {
+ if ((1 + bytesPerValue) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH)
+ {
+ throw new IllegalStateException("too many nodes; increase maxPointsInLeafNode (currently " + maxPointsInLeafNode + ") and reindex");
+ }
+ }
+
+ private void writeCommonPrefix(DataOutput treeOutput, int commonPrefixLength, byte[] packedValue) throws IOException
+ {
+ treeOutput.writeVInt(commonPrefixLength);
+ if (commonPrefixLength > 0)
+ treeOutput.writeBytes(packedValue, 0, commonPrefixLength);
+ }
+
+ private void writeLeafBlockPackedValues(DataOutput out, int commonPrefixLength, int count, IntFunction<BytesRef> packedValues) throws IOException
+ {
+ if (commonPrefixLength == bytesPerValue)
+ {
+ // all values in this block are equal
+ out.writeByte((byte) -1);
+ }
+ else
+ {
+ assert commonPrefixLength < bytesPerValue;
Review Comment:
nit: Throw in a quick error message if we actually keep this I guess...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228651050
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
+ }
+
+ @Override
+ public int getDocID(int i)
+ {
+ return 0;
+ }
+
+ @Override
+ public void swap(int i, int j)
+ {
+ throw new IllegalStateException("unexpected sorting");
+ }
+
+ @Override
+ public void intersect(PointValues.IntersectVisitor visitor)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long estimatePointCount(PointValues.IntersectVisitor visitor)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public byte[] getMinPackedValue()
+ {
+ return EMPTY;
+ }
+
+ @Override
+ public byte[] getMaxPackedValue()
+ {
+ return EMPTY;
+ }
+
+ @Override
+ public int getNumDimensions()
+ {
+ return 1;
+ }
+
+ @Override
+ public int getBytesPerDimension()
+ {
+ return 0;
+ }
Review Comment:
Make this abstract?
```
public abstract int getBytesPerDimension();
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229701253
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
Review Comment:
I've copied from the `PostingsWriter` and added a visual representation of the layout.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230131645
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
Review Comment:
> One option here would be to change how we build the in-memory numerics such that they are naturally sorted. I am thinking of a trie here.
I like this, and I like it a lot. I'm +100 on just replacing `BlockBalancedTreeRamBuffer` entirely w/ an in-memory trie and blowing away all the `PointValues` garbage. Then we end up having something more like `ImmutableIntersectingPointValues` for all segment building cases?
What do we lose if we toss out `BlockBalancedTreeRamBuffer`? `PackedLongValues` packing row IDs more efficiently than the trie stores them as payloads? If losing that and gaining value prefix compression was about break-even, being able to avoid sorting would still be a win...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232952731
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
Review Comment:
This is a hot path, so it seems like spending a little extra effort to size the PQ might be worth it. We do have some metadata about the tree in the `SegmentMetadata`, right? For instance, if we had a tree w/ 60 leaf nodes (and so "num_leaf_postings" == 60), and `MINIMUM_POSTINGS_LEAVES` is the default 64, we know that the worst we could do here is size the PQ to 60, rather than 100?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232952731
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
Review Comment:
This is a hot path, so it seems like spending a little extra effort to size the PQ might be worth it. We do have some metadata about the tree in the `SegmentMetadata`, right? For instance, if we had a tree w/ 60 leaf nodes, and `MINIMUM_POSTINGS_LEAVES` is the default 64, we know that the worst we could do here is size the PQ to 60, rather than 100?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232957608
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
Review Comment:
nit: I guess we remove this comment now?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230147311
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
Review Comment:
Agreed, although if we replace `BlockBalancedTreeRamBuffer` w/ an in-memory trie, `IntersectingPointValues` might go away entirely...so maybe we wait for that?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1243961795
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final class PackedIndexTree
+ {
+ private final byte[][] splitPackedValueStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ // used to read the packed byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the per-dim delta we read for the node at this level is a negative offset vs. the last split on this dim; this is a packed
+ // 2D array, i.e. to access array[level][dim] you read from negativeDeltas[level*numDims+dim]. this will be true if the last time we
+ // split on this dimension, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+ // scratch value to return from getPackedValue:
+ private final BytesRef scratch;
+
+ PackedIndexTree()
+ {
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[packedBytesLength];
+ readNodeData(false);
+ scratch = new BytesRef();
+ scratch.length = bytesPerValue;
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
Review Comment:
Agreed. Firstly, I have removed the `leafNode` flag in favour of just using `isLeafNode`. I've also moved the assertion to the start of the method. The only reason I can find for the existence of the `leafNode` flag was that the `nodeID` changed above this assertion.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1243971054
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
Review Comment:
I have got to the bottom of this part of the code and have established that we don't need the `splitPackedValuesStack` and I have removed this part of the code. I have also removed all the copying forward and backward between different values as we recursed. AFAICT this was done because in the KDTree we were moving over an n-dimensional space rather than a single dimension. Overall, this has made the traversing code a lot simpler.
I now have an understanding of the balancing operation and of the overall structure of the resulting tree. I am just working through a few final edge conditions and I will get some documentation in place for these methods.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247227995
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.db.memtable.TrieMemtable;
+import org.apache.cassandra.db.tries.InMemoryTrie;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.bytecomparable.ByteSource;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for values that provides a sorted view of itself as a {@link BlockBalancedTreeIterator}.
+ */
+@NotThreadSafe
+public class BlockBalancedTreeRamBuffer
+{
+ private final InMemoryTrie<PackedLongValues.Builder> trieBuffer;
+ private final RowIDReducer reducer;
+ private final int bytesPerValue;
+ private int numRows;
+
+ public BlockBalancedTreeRamBuffer(int bytesPerValue)
+ {
+ trieBuffer = new InMemoryTrie<>(TrieMemtable.BUFFER_TYPE);
+ reducer = new RowIDReducer();
+ this.bytesPerValue = bytesPerValue;
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long memoryUsed()
+ {
+ return trieBuffer.sizeOnHeap() + reducer.heapAllocations();
+ }
+
+ public long add(int segmentRowId, byte[] value)
+ {
+ final long initialSizeOnHeap = trieBuffer.sizeOnHeap();
+ final long reducerHeapSize = reducer.heapAllocations();
+
+ try
+ {
+ trieBuffer.putRecursive(v -> ByteSource.fixedLength(value), segmentRowId, reducer);
+ }
+ catch (InMemoryTrie.SpaceExhaustedException e)
+ {
+ throw Throwables.unchecked(e);
+ }
+
+ numRows++;
+ return (trieBuffer.sizeOnHeap() - initialSizeOnHeap) + (reducer.heapAllocations() - reducerHeapSize);
+ }
+
+ public BlockBalancedTreeIterator iterator()
+ {
+ return BlockBalancedTreeIterator.fromTrieIterator(trieBuffer.entrySet().iterator(), bytesPerValue);
+ }
+
+ private static class RowIDReducer implements InMemoryTrie.UpsertTransformer<PackedLongValues.Builder, Integer>
+ {
+ private final LongAdder heapAllocations = new LongAdder();
+
+ @Override
+ public PackedLongValues.Builder apply(PackedLongValues.Builder existing, Integer neww)
Review Comment:
```suggestion
public PackedLongValues.Builder apply(PackedLongValues.Builder existing, Integer update)
```
nit...feel free to ignore
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247229705
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.db.memtable.TrieMemtable;
+import org.apache.cassandra.db.tries.InMemoryTrie;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.bytecomparable.ByteSource;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for values that provides a sorted view of itself as a {@link BlockBalancedTreeIterator}.
+ */
+@NotThreadSafe
+public class BlockBalancedTreeRamBuffer
Review Comment:
I'll avoid making any naming suggestions here, given I'm not sure if we'll reuse this shortly as a string buffer as well...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1246438957
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+ private final long maxRows;
+
+ public BlockBalancedTreeWriter(long maxRows, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxRows = maxRows;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxRows), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> leafWriter.add(packedValue, rowID));
+
+ valueCount = leafWriter.finish();
+
+ long treeFilePointer = valueCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ // There is only any point in writing the balanced tree if any values were added
+ if (treeFilePointer >= 0)
+ writeBalancedTree(treeOutput, maxPointsInLeafNode, leafWriter.leafBlockStartValues, leafWriter.leafBlockFilePointers);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return treeFilePointer;
+ }
+
+ private void writeBalancedTree(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitValues = new byte[(1 + numInnerNodes) * bytesPerValue];
+ int depth = recurseBalanceTree(1, 0, numInnerNodes, 1, splitValues, leafBlockStartValues);
Review Comment:
See above
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247608718
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final TraversalState state;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInput indexInput = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(indexInput);
+ indexInput.seek(treeIndexRoot);
+
+ state = new TraversalState(indexInput);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return state.memoryUsage;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ state.reset();
+ traverse(callback, new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, IntArrayList pathToRoot)
+ {
+ if (state.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (state.nodeExists())
+ {
+ callback.onLeaf(state.nodeID, state.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(state.nodeID);
+
+ state.pushLeft();
+ traverse(callback, currentPath);
+ state.pop();
+
+ state.pushRight();
+ traverse(callback, currentPath);
+ state.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ /**
+ * This maintains the state for a traversal of the packed index. It is loaded once and can be resused
+ * by calling the reset method.
+ * <p>
+ * The packed index is a packed representation of a balanced tree and takes the form of a packed array of
+ * file pointer / split value pairs. Both the file pointers and split values are prefix compressed by tree level
+ * requiring us to maintain a stack of values for each level in the tree. The stack size is always the tree depth.
+ * <p>
+ * The tree is traversed by recursively following the left and then right subtrees under the current node. For the
+ * following tree (split values in square brackets):
+ * <pre>
+ * 1[16]
+ * / \
+ * / \
+ * 2[8] 3[24]
+ * / \ / \
+ * 4 5 6 7
+ * </pre>
+ * The traversal will be 1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 with nodes 4, 5, 6 & 7 being leaf nodes.
+ * <p>
+ * Assuming the full range of values in the tree is 0 -> 32, the non-leaf nodes will represent the following
+ * values:
+ * <pre>
+ * 1[0-32]
+ * / \
+ * 2[0-16] 3[16-32]
Review Comment:
This shows a deficiency in my ascii art skills. These we not meant to imply inclusivity or not. This may be an area for improvement in the future but at the moment both nodes can, effectively, own that value.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247612311
##########
src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java:
##########
@@ -123,7 +128,6 @@ public PostingsWriter(IndexDescriptor indexDescriptor, IndexContext indexContext
private PostingsWriter(IndexOutput dataOutput, int blockSize) throws IOException
{
assert dataOutput instanceof IndexOutputWriter;
Review Comment:
I nearly removed this when I removed the spamming log statement below it. I have made this change now.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228337628
##########
src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java:
##########
@@ -130,6 +131,23 @@ public static ByteComparable max(ByteComparable a, ByteComparable b)
return a == null ? b : (b == null || ByteComparable.compare(b, a, ByteComparable.Version.OSS50) < 0) ? a : b;
}
+ /**
+ * Returns the value length for the given {@link AbstractType}, selecting 16 for types
+ * that officially use VARIABLE_LENGTH but are, in fact, of a fixed length.
+ */
+ public static int fixedSizeOf(AbstractType<?> type)
+ {
+ if (type.isValueLengthFixed())
+ return type.valueLengthIfFixed();
+ else if (isInetAddress(type))
+ return 16;
+ else if (isBigInteger(type))
+ return 20;
Review Comment:
nit: Replace 16 and 20 magic numbers w/ constants (see https://github.com/apache/cassandra/pull/2409/files#r1227304071)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232817833
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
Review Comment:
nit: Not sure if `CorruptIndexException` or `ISE` makes more sense here.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232547884
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+
+public class BlockBalancedTreeQueries
+{
+ private static final BlockBalancedTreeReader.IntersectVisitor MATCH_ALL = new BlockBalancedTreeReader.IntersectVisitor()
+ {
+ @Override
+ public boolean visit(byte[] packedValue)
+ {
+ return true;
+ }
+
+ @Override
+ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ return Relation.CELL_INSIDE_QUERY;
+ }
+ };
+
+ public static BlockBalancedTreeReader.IntersectVisitor balancedTreeQueryFrom(Expression expression, int bytesPerValue)
+ {
+ if (expression.lower == null && expression.upper == null)
+ {
+ return MATCH_ALL;
Review Comment:
nit: untested code path?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232863495
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+
+public class BlockBalancedTreeQueries
+{
+ private static final BlockBalancedTreeReader.IntersectVisitor MATCH_ALL = new BlockBalancedTreeReader.IntersectVisitor()
+ {
+ @Override
+ public boolean visit(byte[] packedValue)
+ {
+ return true;
+ }
+
+ @Override
+ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ return Relation.CELL_INSIDE_QUERY;
+ }
+ };
+
+ public static BlockBalancedTreeReader.IntersectVisitor balancedTreeQueryFrom(Expression expression, int bytesPerValue)
+ {
+ if (expression.lower == null && expression.upper == null)
+ {
+ return MATCH_ALL;
+ }
+
+ Bound lower = null ;
+ if (expression.lower != null)
+ {
+ final byte[] lowerBound = toComparableBytes(bytesPerValue, expression.lower.value.encoded, expression.validator);
+ lower = new Bound(lowerBound, !expression.lower.inclusive);
+ }
+
+ Bound upper = null;
+ if (expression.upper != null)
+ {
+ final byte[] upperBound = toComparableBytes(bytesPerValue, expression.upper.value.encoded, expression.validator);
+ upper = new Bound(upperBound, !expression.upper.inclusive);
+ }
+
+ return new RangeQueryVisitor(bytesPerValue, lower, upper);
+ }
+
+ private static byte[] toComparableBytes(int bytesPerDim, ByteBuffer value, AbstractType<?> type)
+ {
+ byte[] buffer = new byte[TypeUtil.fixedSizeOf(type)];
+ assert buffer.length == bytesPerDim;
+ TypeUtil.toComparableBytes(value, type, buffer);
+ return buffer;
+ }
+
+ private static abstract class RangeQuery implements BlockBalancedTreeReader.IntersectVisitor
+ {
+ final int bytesPerValue;
+
+ RangeQuery(int bytesPerValue)
+ {
+ this.bytesPerValue = bytesPerValue;
+ }
+
+ int compareUnsigned(byte[] packedValue, Bound bound)
Review Comment:
It feels like the code that uses this would be clearer if we...
1.) Removed `compareUnsigned()` here.
2.) Changed `Bound` to look like...
```
boolean smallerThan(byte[] packedValue)
{
int cmp = compareTo(packedValue, bound.length);
return cmp < 0 || (cmp == 0 && exclusive);
}
boolean greaterThan(byte[] packedValue)
{
int cmp = compareTo(packedValue, bound.length);
return cmp > 0 || (cmp == 0 && exclusive);
}
private int compareTo(byte[] packedValue, int length)
{
assert packedValue.length == length : "WTF";
return FutureArrays.compareUnsigned(bound, 0, length, packedValue, 0, length);
}
```
3.) Changed usages to look like `if (lower.greaterThan(maxPackedValue))`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227304071
##########
src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java:
##########
@@ -186,9 +204,38 @@ public static ByteSource asComparableBytes(ByteBuffer value, AbstractType<?> typ
{
if (type instanceof InetAddressType || type instanceof IntegerType || type instanceof DecimalType)
return ByteSource.optionalFixedLength(ByteBufferAccessor.instance, value);
+ // The LongType.asComparableBytes uses variableLengthInteger which doesn't play well with
+ // the KDTree because it is expecting fixed length data. So for SAI we use a optionalSignedFixedLengthNumber
+ // to keep all comparable values the same length
+ else if (type instanceof LongType)
+ return ByteSource.optionalSignedFixedLengthNumber(ByteBufferAccessor.instance, value);
return type.asComparableBytes(value, version);
}
+ /**
+ * Fills a byte array with the comparable bytes for a type.
+ * <p>
+ * This method expects a {@code value} parameter generated by calling {@link #asIndexBytes(ByteBuffer, AbstractType)}.
+ * It is not generally safe to pass the output of other serialization methods to this method. For instance, it is
+ * not generally safe to pass the output of {@link AbstractType#decompose(Object)} as the {@code value} parameter
+ * (there are certain types for which this is technically OK, but that doesn't hold for all types).
+ *
+ * @param value a value buffer returned by {@link #asIndexBytes(ByteBuffer, AbstractType)}
+ * @param type the type associated with the encoded {@code value} parameter
+ * @param bytes this method's output
+ */
+ public static void toComparableBytes(ByteBuffer value, AbstractType<?> type, byte[] bytes)
+ {
+ if (isInetAddress(type))
+ ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, 16);
+ else if (isBigInteger(type))
+ ByteBufferUtil.copyBytes(value, value.hasArray() ? value.arrayOffset() + value.position() : value.position(), bytes, 0, 20);
Review Comment:
nit: Replace the magic 20 and 16 w/ named constants like `DECIMAL_APPROXIMATION_BYTES`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228807024
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
Review Comment:
Possible alternative signature:
```
public long writeTree(IndexOutput out, IntersectingPointValues reader, Callback callback) throws IOException
```
Or it could just be `write()`. I guess we know what we're writing without further qualification, right? :)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227305793
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
+
+ docIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(docIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ if (value.length != packedBytesLength)
+ {
+ throw new IllegalArgumentException("The value has length=" + value.length + " but should be " + pointNumBytes);
+ }
+
+ long startingBytesUsed = bytesUsed.get();
+ long startingDocIDsBytesUsed = docIDsBuilder.ramBytesUsed();
+
+ docIDsBuilder.add(segmentRowId);
+ bytes.append(value);
+
+ if (segmentRowId != lastSegmentRowID)
+ {
+ numRows++;
+ lastSegmentRowID = segmentRowId;
+ }
+
+ numPoints++;
+
+ long docIDsAllocatedBytes = docIDsBuilder.ramBytesUsed() - startingDocIDsBytesUsed;
+ long endingBytesAllocated = bytesUsed.addAndGet(docIDsAllocatedBytes);
+
+ return endingBytesAllocated - startingBytesUsed;
+ }
+
+ public MutableOneDimPointValues asPointValues()
+ {
+ ensureOpen();
+ // building packed longs is destructive
+ closed = true;
+ final PackedLongValues docIDs = docIDsBuilder.build();
Review Comment:
```suggestion
final PackedLongValues rowIDs = docIDsBuilder.build();
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228181444
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java:
##########
@@ -71,10 +73,48 @@
private ByteBuffer minTerm;
private ByteBuffer maxTerm;
+ final AbstractType<?> termComparator;
long totalBytesAllocated;
int rowCount = 0;
int maxSegmentRowId = -1;
+ public static class KDTreeSegmentBuilder extends SegmentBuilder
+ {
+ protected final byte[] buffer;
Review Comment:
That is correct, but...
This is only used in `addInternal` and then a `BytesRef` is created as a wrapper around this buffer in order to add it to the `ByteBlockPool`. In the `ByteBlockPool` the buffer is copied so it is safe to change this to create a `BytesRef` so we don't need to create a new one for each add operation.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228119977
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible
+ * tree levels.
+ *
+ * Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to
+ * buffer all point values to sort them.
+ */
Review Comment:
```suggestion
/**
* Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
* posting lists on eligible tree levels.
* <p>
* Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process,
* because we don't need to buffer all point values to sort them.
*/
```
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible
+ * tree levels.
+ *
+ * Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for kd-tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.OneDimensionBKDWriterCallback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a k-d tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this kd-tree in the overall SSTable kd-tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
Review Comment:
This method has multiple vars using the `bkd` prefix. Maybe we could just refer to the block balanced tree in the context of this class as just "the tree", and name those vars `treePosition`, `treeOutput`, `treeOffset`, `treeLength`, etc.
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,769 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * See <a href="https://www.cs.duke.edu/~pankaj/publications/papers/bkd-sstd.pdf">this paper</a> for details.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final OneDimensionBKDWriterCallback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface OneDimensionBKDWriterCallback
Review Comment:
Perhaps we might call this inner class just `Callback`?
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible
+ * tree levels.
+ *
+ * Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
Review Comment:
If `BlockBalancedTree*` is the type of tree that we use for indexing numbers in `NumericIndexWriter`, would it make any sense to simply call the tree classes `NumericTree*`? Meaning that it's the tree that we use for numbers, independently of what kind of tree it is. I guess the advantage would be having a shorter name that is possibly clearer in most of the contexts where it's used.
##########
src/java/org/apache/cassandra/index/sai/metrics/QueryEventListener.java:
##########
@@ -24,6 +24,37 @@
*/
public interface QueryEventListener
{
+ /**
+ * Collector for kd-tree index file related metrics.
+ */
+ interface BKDIndexEventListener
Review Comment:
Maybe we could rename this class to `BlockBalancedTreeEventListener`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228144604
##########
src/java/org/apache/cassandra/index/sai/disk/io/RAMIndexOutput.java:
##########
@@ -70,11 +75,16 @@ public void writeTo(IndexOutput externalOut) throws IOException
externalOut.writeBytes(out.getBytes(), 0, out.getPosition());
}
- public BytesRef getBytes()
+ public BytesRef getBytesRef()
Review Comment:
I have moved `getBytesRef` up the class but I have also removed `getBytes` because it was only being used in a test and the call can be replaced with `getBytesRef.bytes`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228302180
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible
+ * tree levels.
+ *
+ * Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for kd-tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.OneDimensionBKDWriterCallback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a k-d tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this kd-tree in the overall SSTable kd-tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
Review Comment:
`balancedTree` as prefix for tree things sounds good to me. One less word, and repeating the `block` part once and again in a tree structure inside the disk package doesn't add too much value.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228830151
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
Review Comment:
nit: Especially w/ the callback here, maybe a full JavaDoc w/ `@params` would be useful here. In the general method description, perhaps summarize that we're writing the header, the tree itself, and then a footer. Then for `@return`, the file position of the...footer?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228973163
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class OneDimensionBKDWriter
+ {
+ private final IndexOutput out;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafDocs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafCount;
Review Comment:
```suggestion
private int leafValueCount;
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228970020
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
Review Comment:
So to summarize my understanding of how this works, we start w/ a `IntersectingPointValues`. We hit `writeField()` (which I've suggested renaming, etc.), write a footer, pass all the points to `OneDimensionBKDWriter`, finalize the tree in `OneDimensionBKDWriter`, write a footer, and return a the footer position. While that's happening, `OneDimensionBKDWriter` also sends blocks postings lists through the provided callback, and those are later written to the actual postings file, along w/ the auxiliary postings. `BlockBalancedTreeWriter` really doesn't do much other than store some configuration parameters for the process (ex. `bytesPerValue`), and those inform `OneDimensionBKDWriter`, which actually does all the work.
How do we actually want to document this? Right now, we have no high level documentation on `OneDimensionBKDWriter`, and the documentation here on `BlockBalancedTreeWriter` is sort of random details copied from (I'm guessing?) the Lucene JavaDoc. (I could be wrong there.) Here's one proposal for how we do this:
1.) Provide a decent summary of what `writeField()` does below, see https://github.com/apache/cassandra/pull/2409/files#r1228830151
2.) Whittle down the class level JavaDoc (especially the "smaller and smaller rectangles" bit) to `@see` reference `BKDWriter`, retain the bit about `maxPointsInLeafNode` if it still matters (although w/ `{@link #maxPointsInLeafNode}`), and describe the top-level format, which is just header, tree (written by `OneDimensionBKDWriter` or whatever we call it), and a footer.
3.) In the class JavaDoc for `OneDimensionBKDWriter`, actually describe the format for the tree, with the leaf nodes, then the packed index representing the binary tree. The question here is whether we want to describe the leaf format, w/ the order index and the packed values in detail, but I think we do. The alternative would be pushing it down to `writeLeafBlock()`, but that feels too fragmented.
4.) Have short summary JavaDoc for `add()` and `finish()` that roughly describe their processes.
5.) Document everything below that level w/ inline comments where they seem useful.
It seems like there are some TODOs (which I haven't fully wrapped my head around) in the tree building logic below, but even if some of that changes, what I've described in these 5 points doesn't seem very likely to change.
WDYT?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230186717
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
Review Comment:
I was having trouble hitting this w/ the tests, and then I realized that most of them don't actually full up a leaf. For instance, I took `NumericIndexWriterTest#shouldFlushFromMemtable()` and boosted to `final int maxSegmentRowId = NumericIndexWriter.MAX_POINTS_IN_LEAF_NODE * 3;`. This and all the other code for rotating the tree, etc. is then hit.
Are there actually other tests that write more than a single leaf, or do we need to pick some places like this ^ to extend coverage?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239655803
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
+
+ docIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(docIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ if (value.length != packedBytesLength)
+ {
+ throw new IllegalArgumentException("The value has length=" + value.length + " but should be " + pointNumBytes);
+ }
+
+ long startingBytesUsed = bytesUsed.get();
+ long startingDocIDsBytesUsed = docIDsBuilder.ramBytesUsed();
+
+ docIDsBuilder.add(segmentRowId);
+ bytes.append(value);
+
+ if (segmentRowId != lastSegmentRowID)
+ {
+ numRows++;
+ lastSegmentRowID = segmentRowId;
+ }
+
+ numPoints++;
+
+ long docIDsAllocatedBytes = docIDsBuilder.ramBytesUsed() - startingDocIDsBytesUsed;
+ long endingBytesAllocated = bytesUsed.addAndGet(docIDsAllocatedBytes);
+
+ return endingBytesAllocated - startingBytesUsed;
+ }
+
+ public MutableOneDimPointValues asPointValues()
+ {
+ ensureOpen();
+ // building packed longs is destructive
+ closed = true;
+ final PackedLongValues docIDs = docIDsBuilder.build();
+ return new MutableOneDimPointValues()
+ {
+ final int[] ords = new int[numPoints];
+
+ {
+ for (int i = 0; i < numPoints; ++i)
+ {
+ ords[i] = i;
+ }
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ final long offset = (long) packedBytesLength * (long) ords[i];
+ packedValue.length = packedBytesLength;
+ bytes.setRawBytesRef(packedValue, offset);
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ final long offset = (long) packedBytesLength * (long) ords[i] + (long) k;
+
+ return bytes.readByte(offset);
+ }
+
+ @Override
+ public int getDocID(int i)
+ {
+ return Math.toIntExact(docIDs.get(ords[i]));
+ }
+
+ @Override
+ public void swap(int i, int j)
+ {
+ int tmp = ords[i];
+ ords[i] = ords[j];
+ ords[j] = tmp;
+ }
+
+ @Override
+ public void intersect(IntersectVisitor visitor) throws IOException
Review Comment:
It have thought more about this and have decided that we leave this as `intersect` for now but with [CASSANDRA-18598](https://issues.apache.org/jira/browse/CASSANDRA-18598) we will have no dependency on Lucene `PointValues` and can remove the `intersection` / `visitor` patterns completely in favour of making the reader `Iterable`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239685024
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
Review Comment:
Correct, I have changed the javadoc.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1240149506
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes leaf postings and auxiliary posting lists for bbtree nodes. If a node has a posting list attached,
+ * it will contain every row id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * The leaf blocks are written in value order (in the order we pass them to the {@link BlockBalancedTreeWriter}).
+ * This allows us to skip reading the leaves, instead just order leaf blocks by their offset in the index file,
+ * and correlate them with buffered posting lists.
+ */
+@NotThreadSafe
+public class BlockBalancedTreePostingsWriter implements BlockBalancedTreeWalker.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
+ private final TreeMap<Long, Integer> leafOffsetToNodeID = new TreeMap<>(Long::compareTo);
+ private final Multimap<Integer, Integer> nodeToChildLeaves = HashMultimap.create();
+
+ private final IndexContext indexContext;
+ int numNonLeafPostings = 0;
+ int numLeafPostings = 0;
+
+ BlockBalancedTreePostingsWriter(List<PackedLongValues> postings, IndexContext indexContext)
+ {
+ this.postings = postings;
+ this.indexContext = indexContext;
+ }
+
+ /**
+ * Called when a leaf node is hit as we traverse the packed index.
+ *
+ * @param leafNodeID the current leaf node ID in the packed inded
+ * @param leafBlockFP the file pointer to the on-disk leaf block
+ * @param pathToRoot the path to the root leaf above this leaf. Contains all the intermediate leaf node IDs.
+ */
+ @Override
+ public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot)
+ {
+ checkArgument(!pathToRoot.containsInt(leafNodeID));
+ checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1));
+
+ leafOffsetToNodeID.put(leafBlockFP, leafNodeID);
+ for (int i = 0; i < pathToRoot.size(); i++)
+ {
+ int level = i + 1;
+ if (isLevelEligibleForPostingList(level))
+ {
+ int nodeID = pathToRoot.get(i);
+ nodeToChildLeaves.put(nodeID, leafNodeID);
+ }
+ }
+ }
+
+ /**
+ * Writes a merged posting list for each leaf in the tree. This postings list consists of the postings
Review Comment:
nit: Leaf postings don't have to be merged?
Alternate wording...
```
* Writes merged posting lists for eligible internal nodes and leaf postings for each leaf in the tree.
* The merged postings list for an internal node contains all postings from the postings lists of leaf nodes
* in the subtree rooted at that node.
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1234547867
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
Review Comment:
nit: I'm puzzling about the `splitDimValue` naming here. We've only got one dimension, so does `splitValuesStack` need to be a two-dimensional array? (i.e. Is there only one split value per level?)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242367814
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
Review Comment:
Yes, I like that. I have carried that through to `buildPostingsFilterForSingleValueLeaf` and `buildPostingsFilterForMultValueLeaf`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242089721
##########
src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java:
##########
@@ -235,7 +235,7 @@ public int openFilesPerSSTableIndex()
@Override
public int openFilesPerColumnIndex(IndexContext indexContext)
{
- // For the V1 format there are always 2 open files per index - index (kdtree or terms) + postings
+ // For the V1 format there are always 2 open files per index - index (balanced tree or terms) + postings
Review Comment:
I think we only need to mention that one of the files here is the postings file. This method is about how many files are open for an index. It doesn't need to go into specifics of what are in the files. We do that elsewhere.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242454614
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
Review Comment:
Hmm, so this opened a bit of a can of worms. I need to look at this in much more detail. I removed both of these from the method and from the `TraversalState` (was `PackedIndexTree`) and all the tests still pass.
As I look at it, it seems that most of this was in place to handle the splitting around multiple dimensions and because we don't need them. We can just pass the min/max packed values through the recursion because the split dimension never changes.
If this is the case we don't need to store some of this information in the in-memory state which could significantly reduce the in-memory requirement for the indexes.
I need to do a bit more investigation of this to make sure that I'm not missing anything.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232565377
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(treeOutput.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
Review Comment:
nit: You've probably already changed this, but it's just on the one dimension now I guess...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227216477
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDWriter.java:
##########
@@ -0,0 +1,782 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.LongBitSet;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension.
+ * <p>
+ * Recursively builds a block KD-tree to assign all incoming points in N-dim space to smaller
+ * and smaller N-dim rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * See <a href="https://www.cs.duke.edu/~pankaj/publications/papers/bkd-sstd.pdf">this paper</a> for details.
+ *
+ * <p>This consumes heap during writing: it allocates a <code>LongBitSet(numPoints)</code>,
+ * and then uses up to the specified {@code maxMBSortInHeap} heap space for writing.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ *
+ * lucene.experimental
+ */
+public class BKDWriter
Review Comment:
So let's have the naming discussion :)
I guess the first thing to establish is whether or not we're ever going to try to actually use the kd-tree for geo-spatial search in the future. It seems like a possibility, but not in 5.0...and who knows when after that.
Assuming we ignore the future, we're left w/ how to name `BKDWriter` and `OneDimensionBKDWriter`. `BKDWriter` seems like a poor choice, given it both doesn't connote the single-dimension aspect of what it does, and it has the same name as the Lucene class it's loosely based on. All it really is now is a writer for a block-oriented, balanced, binary tree, optimized to be immutable on disk. We could call it a lot of things, but I'll just throw some out and see what sticks w/ everyone:
`BlockTreeWriter`, `BlockBalancedTreeWriter`, `BlockBinaryTreeWriter`, etc.
(At least a few people have asked why we even have this tree when the trie exists, but emphasizing that it's a **balanced** tree and better for large range queries, even in our naming, might be nice.)
`OneDimensionBKDWriter` is private, so it doesn't matter much what we name it I guess, but I would consider something like `BlockWriter` or `LeafWriter`, and I would move the `writeIndex()` call out of `finish()` and into the top-level `writeField()` (which I might also rename `writeAll()` or `writeTerms()`, etc.) That way it does one thing (writes blocks) and lets the top-level writer tie everything together.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229372243
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
+ }
+
+ @Override
+ public int getDocID(int i)
+ {
+ return 0;
+ }
+
+ @Override
+ public void swap(int i, int j)
+ {
+ throw new IllegalStateException("unexpected sorting");
+ }
+
+ @Override
+ public void intersect(PointValues.IntersectVisitor visitor)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long estimatePointCount(PointValues.IntersectVisitor visitor)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public byte[] getMinPackedValue()
+ {
+ return EMPTY;
+ }
+
+ @Override
+ public byte[] getMaxPackedValue()
+ {
+ return EMPTY;
+ }
+
+ @Override
+ public int getNumDimensions()
+ {
+ return 1;
+ }
+
+ @Override
+ public int getBytesPerDimension()
+ {
+ return 0;
+ }
+
+ public interface IntersectVisitor
+ {
+ /**
+ * Called for all documents in a leaf cell that crosses the query. The consumer should scrutinize the
Review Comment:
I think `values` is more appropriate, I have changed it to that.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229603232
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
Review Comment:
My initial answer is no because we have yet to determine how many leaves we will end up with. I have given the list a default size of 128 because that avoids `grow` calls. I can add this to a list of things to investigate when we do performance/load testing. It would be good to see how many leaves we end up with in a segment.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227256794
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java:
##########
@@ -71,10 +73,48 @@
private ByteBuffer minTerm;
private ByteBuffer maxTerm;
+ final AbstractType<?> termComparator;
long totalBytesAllocated;
int rowCount = 0;
int maxSegmentRowId = -1;
+ public static class KDTreeSegmentBuilder extends SegmentBuilder
+ {
+ protected final byte[] buffer;
Review Comment:
Just to confirm my understanding, this buffer is constantly being overwritten/reused, but safely in a single-threaded context?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228189264
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
+
+ docIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(docIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ if (value.length != packedBytesLength)
+ {
+ throw new IllegalArgumentException("The value has length=" + value.length + " but should be " + pointNumBytes);
+ }
Review Comment:
Agreed, I have been trying to rationalise the use of assertions and exceptions in the code.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228439049
##########
src/java/org/apache/cassandra/config/CassandraRelevantProperties.java:
##########
@@ -399,6 +399,17 @@
SAI_LATEST_VERSION("cassandra.sai.latest_version", "aa"),
SAI_MAX_FROZEN_TERM_SIZE("cassandra.sai.max_frozen_term_size_kb", "5"),
SAI_MAX_STRING_TERM_SIZE("cassandra.sai.max_string_term_size_kb", "1"),
+
+ /** Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list */
+ SAI_MINIMUM_POSTINGS_LEAVES("cassandra.sai.minimum_postings_leaves", "64"),
+
+ /**
+ * Skip, or the sampling interval, for selecting a bkd tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but bkd tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
Review Comment:
nit/TODO: "bkd" usage cleanup
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229357281
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
Review Comment:
I have been looking at the current Lucene implementation and a lot of this weirdness has been fixed on their trunk. Specifically, they have changed the format of the tree when it is built so they don't need to then do the `rotateToTree` to rebuild it in order to write it to disk. These changes are primarily for the one-dimensional case so are very relevant to us. I am thinking that some of these changes could be pulled in as part of a separate ticket for phase 2.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229756334
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
Review Comment:
I agree with all of this. I have made a start to this with what I have described above but I'd like to go much further on the javadoc to be absolutely clear about what this class is doing. There are a number of methods that really need some form of documentation to describe what they are doing - `rotateToTree` being the main culprit. I'm going to do some digging into this method tomorrow to establish exactly what it is doing and why. I mentioned above that this method is gone in the latest Lucene implementation but for the time being we should describe what it is doing.
The actual structure of the index isn't that complex but I will try and add as much detail as possible to the javadoc. I may even pull the structure up to the class level rather than leaving it on `writeTree`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239710318
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(treeOutput.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
+ int commonPrefixLength = bytesPerValue;
+ int offset = (leafValueCount - 1) * bytesPerValue;
+ for (int j = 0; j < bytesPerValue; j++)
+ {
+ if (leafValues[j] != leafValues[offset + j])
+ {
+ commonPrefixLength = j;
+ break;
+ }
+ }
+
+ assert scratchOut.getPosition() == 0;
+
+ treeOutput.writeVInt(leafValueCount);
+
+ for (int x = 0; x < leafValueCount; x++)
+ {
+ rowIDAndIndexes[x].valueOrderIndex = x;
+ rowIDAndIndexes[x].rowID = leafRowIDs[x];
+ }
+
+ final Sorter sorter = new IntroSorter()
+ {
+ RowIDAndIndex pivot;
+
+ @Override
+ protected void swap(int i, int j)
+ {
+ RowIDAndIndex o = rowIDAndIndexes[i];
+ rowIDAndIndexes[i] = rowIDAndIndexes[j];
+ rowIDAndIndexes[j] = o;
+ }
+
+ @Override
+ protected void setPivot(int i)
+ {
+ pivot = rowIDAndIndexes[i];
+ }
+
+ @Override
+ protected int comparePivot(int j)
+ {
+ return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID);
+ }
+ };
+
+ sorter.sort(0, leafValueCount);
+
+ // write leaf rowID -> orig index
+ scratchOut2.reset();
+
+ // iterate in row ID order to get the row ID index for the given value order index
+ // place into an array to be written as packed ints
+ for (int x = 0; x < leafValueCount; x++)
+ orderIndex[rowIDAndIndexes[x].valueOrderIndex] = x;
+
+ LeafOrderMap.write(orderIndex, leafValueCount, maxPointsInLeafNode - 1, scratchOut2);
+
+ treeOutput.writeVInt(scratchOut2.getPosition());
+ treeOutput.writeBytes(scratchOut2.getBytes(), 0, scratchOut2.getPosition());
+
+ callback.writeLeafDocs(rowIDAndIndexes, 0, leafValueCount);
+
+ writeCommonPrefix(scratchOut, commonPrefixLength, leafValues);
+
+ scratchBytesRef1.length = bytesPerValue;
+ scratchBytesRef1.bytes = leafValues;
+
+ IntFunction<BytesRef> packedValues = (i) -> {
+ scratchBytesRef1.offset = bytesPerValue * i;
+ return scratchBytesRef1;
+ };
+ if (DEBUG)
+ valuesInOrderAndBounds(leafValueCount,
+ ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue),
+ ArrayUtil.copyOfSubArray(leafValues, (leafValueCount - 1) * bytesPerValue, leafValueCount * bytesPerValue),
+ packedValues,
+ leafRowIDs);
+
+ writeLeafBlockPackedValues(scratchOut, commonPrefixLength, leafValueCount, packedValues);
+
+ treeOutput.writeBytes(scratchOut.getBytes(), 0, scratchOut.getPosition());
+
+ scratchOut.reset();
+ }
+
+ private void checkMaxLeafNodeCount(int numLeaves)
+ {
+ if ((1 + bytesPerValue) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH)
Review Comment:
It's wrong and I removed it. Originally the packed index contained a dimension for every split value but we don't need that because we have a single dimension.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239671714
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
Review Comment:
We could look at packing the values for trie postings but even if we didn't I still think we would gain more from the trie prefix compression than we would lose from the `PackedLongValues`. I'm also looking at the `RAMStringIndexer` here. We could replace that with exactly the same in-memory structure. Whether it gave us much of an improvement or not, it would reduce code complexity and reduce our reliance on Lucene code.
I would say that this one is a priority phase 2 ticket to try and get into 5.0.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228228248
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
+
+ docIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(docIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ if (value.length != packedBytesLength)
+ {
+ throw new IllegalArgumentException("The value has length=" + value.length + " but should be " + pointNumBytes);
+ }
+
+ long startingBytesUsed = bytesUsed.get();
+ long startingDocIDsBytesUsed = docIDsBuilder.ramBytesUsed();
+
+ docIDsBuilder.add(segmentRowId);
+ bytes.append(value);
+
+ if (segmentRowId != lastSegmentRowID)
+ {
+ numRows++;
+ lastSegmentRowID = segmentRowId;
+ }
+
+ numPoints++;
+
+ long docIDsAllocatedBytes = docIDsBuilder.ramBytesUsed() - startingDocIDsBytesUsed;
+ long endingBytesAllocated = bytesUsed.addAndGet(docIDsAllocatedBytes);
+
+ return endingBytesAllocated - startingBytesUsed;
+ }
+
+ public MutableOneDimPointValues asPointValues()
+ {
+ ensureOpen();
+ // building packed longs is destructive
+ closed = true;
+ final PackedLongValues docIDs = docIDsBuilder.build();
+ return new MutableOneDimPointValues()
+ {
+ final int[] ords = new int[numPoints];
+
+ {
+ for (int i = 0; i < numPoints; ++i)
+ {
+ ords[i] = i;
+ }
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ final long offset = (long) packedBytesLength * (long) ords[i];
+ packedValue.length = packedBytesLength;
+ bytes.setRawBytesRef(packedValue, offset);
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ final long offset = (long) packedBytesLength * (long) ords[i] + (long) k;
+
+ return bytes.readByte(offset);
+ }
+
+ @Override
+ public int getDocID(int i)
+ {
+ return Math.toIntExact(docIDs.get(ords[i]));
+ }
+
+ @Override
+ public void swap(int i, int j)
+ {
+ int tmp = ords[i];
+ ords[i] = ords[j];
+ ords[j] = tmp;
+ }
+
+ @Override
+ public void intersect(IntersectVisitor visitor) throws IOException
Review Comment:
I have been debating this one and came to the conclusion that `intersect` is probably the correct name. Although, in the majority of cases, the implementation is just an iteration of values, it is also used to test values before passing them to the visitor.
I don't feel that `visit` is correct because this isn't the visitor. It could be `visitAll` but, as above, this is not true in all cases.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230771021
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
+ {
+ long balancedTreePosition;
+ SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap();
+
+ LeafCallback leafCallback = new LeafCallback();
+
+ try (IndexOutput balancedTreeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexContext, true))
+ {
+ // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer.
+ long balancedTreeOffset = balancedTreeOutput.getFilePointer();
+
+ balancedTreePosition = writer.writeField(balancedTreeOutput, values, leafCallback);
+
+ // If the balancedTreePosition is less than 0 then we didn't write any values out
+ // and the index is empty
+ if (balancedTreePosition < 0)
+ return components;
+
+ long balancedTreeLength = balancedTreeOutput.getFilePointer() - balancedTreeOffset;
+
+ Map<String, String> attributes = new LinkedHashMap<>();
Review Comment:
I think I answered this earlier - we don't currently do anything with them but I think it's worth leaving them there because we could create offline tooling that made use of them for index analysis.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232971720
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
Review Comment:
We have the common prefix length and the logical bytes per dimension already. Why do we need the "compressed dimension" at all in the on-disk format?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242173723
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BlockBalancedTreeWriter}). That allows us to skip reading the leaves, instead
+ * just order leaf blocks by their offset in the index file, and correlate them with buffered posting lists.
+ */
+public class BlockBalancedTreePostingsWriter implements TraversingBlockBalancedTreeReader.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
+ private final TreeMap<Long, Integer> leafOffsetToNodeID = new TreeMap<>(Long::compareTo);
+ private final Multimap<Integer, Integer> nodeToChildLeaves = HashMultimap.create();
+
+ private final IndexContext indexContext;
+ int numNonLeafPostings = 0;
+ int numLeafPostings = 0;
+
+ BlockBalancedTreePostingsWriter(List<PackedLongValues> postings, IndexContext indexContext)
+ {
+ this.postings = postings;
+ this.indexContext = indexContext;
+ }
+
+ @Override
+ public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot)
+ {
+ checkArgument(!pathToRoot.containsInt(leafNodeID));
+ checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1));
+
+ leafOffsetToNodeID.put(leafBlockFP, leafNodeID);
+ for (int i = 0; i < pathToRoot.size(); i++)
+ {
+ int level = i + 1;
+ if (isLevelEligibleForPostingList(level))
+ {
+ int nodeID = pathToRoot.get(i);
+ nodeToChildLeaves.put(nodeID, leafNodeID);
+ }
+ }
+ }
+
+ public long finish(IndexOutput out) throws IOException
+ {
+ checkState(postings.size() == leafOffsetToNodeID.size(),
+ "Expected equal number of postings lists (%s) and leaf offsets (%s).",
+ postings.size(), leafOffsetToNodeID.size());
+
+ try (PostingsWriter postingsWriter = new PostingsWriter(out))
+ {
+ Iterator<PackedLongValues> postingsIterator = postings.iterator();
+ Map<Integer, PackedLongValues> leafToPostings = new HashMap<>();
+ leafOffsetToNodeID.forEach((fp, nodeID) -> leafToPostings.put(nodeID, postingsIterator.next()));
+
+ long postingsRamBytesUsed = postings.stream()
+ .mapToLong(PackedLongValues::ramBytesUsed)
+ .sum();
+
+ List<Integer> internalNodeIDs = nodeToChildLeaves.keySet()
+ .stream()
+ .filter(i -> nodeToChildLeaves.get(i).size() >= MINIMUM_POSTINGS_LEAVES)
+ .collect(Collectors.toList());
+
+ Collection<Integer> leafNodeIDs = leafOffsetToNodeID.values();
+
+ logger.debug(indexContext.logMessage("Writing posting lists for {} internal and {} leaf balanced tree nodes. Leaf postings memory usage: {}."),
+ internalNodeIDs.size(),
+ leafNodeIDs.size(),
+ FBUtilities.prettyPrintMemory(postingsRamBytesUsed));
+
+ long startFP = out.getFilePointer();
+ Stopwatch flushTime = Stopwatch.createStarted();
+ TreeMap<Integer, Long> nodeIDToPostingsFilePointer = new TreeMap<>();
+ for (int nodeID : Iterables.concat(internalNodeIDs, leafNodeIDs))
+ {
+ Collection<Integer> leaves = nodeToChildLeaves.get(nodeID);
+
+ if (leaves.isEmpty())
+ {
+ leaves = Collections.singletonList(nodeID);
+ numLeafPostings++;
+ }
+ else
+ {
+ numNonLeafPostings++;
+ }
+
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, Comparator.comparingLong(PeekablePostingList::peek));
Review Comment:
Agreed, that would make a lot of sense.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242307156
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final class PackedIndexTree
Review Comment:
Calling it `TraversalState` makes a lot of sense. I've also made it resettable so that we don't have to create a new one for each query or traversal. Creating a new one creates a lot of arrays.
I will have a go at documenting this.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242360634
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
+ // If the compressed dimension is -1 it means that all values in the block are equal
+ if (compressedDimension == -1)
+ return visitRawDocValues(count, visitor, origIndex);
+ else
+ return visitCompressedDocValues(commonPrefixLength, in, count, visitor, origIndex);
+ }
+
+ private int readCompressedDimension(IndexInput in) throws IOException
+ {
+ int compressedDimension = in.readByte();
+ if (compressedDimension != -1 && compressedDimension != 0)
+ throw new CorruptIndexException(String.format("Dimension should be in the range [-1, 0], but was %d.", compressedDimension), in);
+ return compressedDimension;
+ }
+
+ private FixedBitSet visitCompressedDocValues(int commonPrefixLength,
+ IndexInput in,
+ int count,
+ IntersectVisitor visitor,
+ short[] origIndex) throws IOException
+ {
+ // the byte at `compressedByteOffset` is compressed using run-length compression,
+ // other suffix bytes are stored verbatim
+ int compressedByteOffset = commonPrefixLength;
+ commonPrefixLength++;
+ int i;
+
+ FixedBitSet fixedBitSet = new FixedBitSet(maxPointsInLeafNode);
+
+ for (i = 0; i < count; )
+ {
+ packedValue[compressedByteOffset] = in.readByte();
+ final int runLen = Byte.toUnsignedInt(in.readByte());
+ for (int j = 0; j < runLen; ++j)
+ {
+ in.readBytes(packedValue, commonPrefixLength, bytesPerValue - commonPrefixLength);
+ final int rowIDIndex = origIndex[i + j];
+ if (visitor.visit(packedValue))
+ fixedBitSet.set(rowIDIndex);
+ }
+ i += runLen;
+ }
+ if (i != count)
+ throw new CorruptIndexException(String.format("Expected %d sub-blocks but read %d.", count, i), in);
+
+ return fixedBitSet;
+ }
+
+ private FixedBitSet visitRawDocValues(int count, IntersectVisitor visitor, final short[] origIndex)
+ {
+ FixedBitSet fixedBitSet = new FixedBitSet(maxPointsInLeafNode);
+
+ // In our one-dimensional case, all the values in the leaf are the same, so we only
+ // need to visit once then set the bits for the relevant indexes
+ if (visitor.visit(packedValue))
+ {
+ for (int i = 0; i < count; ++i)
+ fixedBitSet.set(origIndex[i]);
Review Comment:
Unfortunately, without rewriting this part of the Lucene code we have to do this here. We rely on the cardinality returned from the `FixedBitset` which isn't available in the `Bits` interface that helpfully implements a `MatchAllBits` and a `MatchNoBits` class.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1240169169
##########
src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java:
##########
@@ -235,7 +235,7 @@ public int openFilesPerSSTableIndex()
@Override
public int openFilesPerColumnIndex(IndexContext indexContext)
{
- // For the V1 format there are always 2 open files per index - index (kdtree or terms) + postings
+ // For the V1 format there are always 2 open files per index - index (balanced tree or terms) + postings
Review Comment:
Looking at `BlockBalancedTreePostingsWriter`, it is both leaf and aux postings in that file. :D
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242115899
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+
+public class BlockBalancedTreeQueries
+{
+ private static final BlockBalancedTreeReader.IntersectVisitor MATCH_ALL = new BlockBalancedTreeReader.IntersectVisitor()
+ {
+ @Override
+ public boolean visit(byte[] packedValue)
+ {
+ return true;
+ }
+
+ @Override
+ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ return Relation.CELL_INSIDE_QUERY;
+ }
+ };
+
+ public static BlockBalancedTreeReader.IntersectVisitor balancedTreeQueryFrom(Expression expression, int bytesPerValue)
+ {
+ if (expression.lower == null && expression.upper == null)
+ {
+ return MATCH_ALL;
Review Comment:
I have added a test to `BalancedBlockTreeQueriesTest` to cover this.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242380475
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
+ // If the compressed dimension is -1 it means that all values in the block are equal
+ if (compressedDimension == -1)
Review Comment:
And, ignoring the above comment, this is correct as well. I'll get rid of the flag entirely and use that as a test.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247232083
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.ResettableByteBuffersIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.Pair;
+import org.apache.lucene.store.ByteBuffersDataOutput;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+
+import static org.apache.cassandra.index.sai.postings.PostingList.END_OF_STREAM;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+
+ public BlockBalancedTreeWriter(int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the sorted values from a {@link BlockBalancedTreeIterator}.
+ * <p>
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param values The {@link BlockBalancedTreeIterator} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, BlockBalancedTreeIterator values, final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ while (values.hasNext())
+ {
+ Pair<byte[], PostingList> pair = values.next();
+ while (true)
+ {
+ long segmentRowId = pair.right.nextPosting();
+ if (segmentRowId == END_OF_STREAM)
+ break;
+ leafWriter.add(pair.left, segmentRowId);
+ }
Review Comment:
nit: Alternate version w/ no unconditional loop:
```
long segmentRowId = pair.right.nextPosting();
while (segmentRowId != END_OF_STREAM)
{
leafWriter.add(pair.left, segmentRowId);
segmentRowId = pair.right.nextPosting();
}
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247628605
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.db.memtable.TrieMemtable;
+import org.apache.cassandra.db.tries.InMemoryTrie;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.bytecomparable.ByteSource;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for values that provides a sorted view of itself as a {@link BlockBalancedTreeIterator}.
+ */
+@NotThreadSafe
+public class BlockBalancedTreeRamBuffer
Review Comment:
Agreed, when we move it we can give it a better name.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247614639
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeIterator.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.index.sai.utils.TermsIterator;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * A helper class for mapping disparate iterators to a single iterator for consumption by the
+ * {@link BlockBalancedTreeWriter}
+ */
+public interface BlockBalancedTreeIterator extends Iterator<Pair<byte[], PostingList>>
Review Comment:
This is why I put it in the `bbtree` package for now. When we implement this for strings we can move it to a more general place with a better name. I'd like to sort out the whole iterator thing as well we have a more generic iterator and don't have so many wrappers for different types.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229704196
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Specialized writer for point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ * <p>
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
+ {
+ long treePosition;
+ SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap();
+
+ LeafCallback leafCallback = new LeafCallback();
+
+ try (IndexOutput treeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexContext, true))
+ {
+ // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer.
+ long treeOffset = treeOutput.getFilePointer();
+
+ treePosition = writer.writeField(treeOutput, values, leafCallback);
+
+ // If the treePosition is less than 0 then we didn't write any values out
+ // and the index is empty
Review Comment:
Nit: no need to split the line
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Specialized writer for point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ * <p>
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
+ {
+ long treePosition;
+ SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap();
+
+ LeafCallback leafCallback = new LeafCallback();
+
+ try (IndexOutput treeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexContext, true))
+ {
+ // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer.
+ long treeOffset = treeOutput.getFilePointer();
+
+ treePosition = writer.writeField(treeOutput, values, leafCallback);
+
+ // If the treePosition is less than 0 then we didn't write any values out
+ // and the index is empty
+ if (treePosition < 0)
+ return components;
+
+ long treeLength = treeOutput.getFilePointer() - treeOffset;
+
+ Map<String, String> attributes = new LinkedHashMap<>();
+ attributes.put("max_points_in_leaf_node", Integer.toString(writer.getMaxPointsInLeafNode()));
+ attributes.put("num_leaves", Integer.toString(leafCallback.numLeaves()));
+ attributes.put("num_points", Long.toString(writer.getPointCount()));
+ attributes.put("bytes_per_dim", Long.toString(writer.getBytesPerValue()));
Review Comment:
Could this be named "bytes_per_value", like the getter, having only one dimension?
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
Review Comment:
This could use some class JavaDoc
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(IndexTreeTraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(IndexTreeTraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(IndexTreeTraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface IndexTreeTraversalCallback
Review Comment:
Maybe this could be named just `TraverseCallback`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1234549457
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final class PackedIndexTree
+ {
+ private final byte[][] splitPackedValueStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ // used to read the packed byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the per-dim delta we read for the node at this level is a negative offset vs. the last split on this dim; this is a packed
+ // 2D array, i.e. to access array[level][dim] you read from negativeDeltas[level*numDims+dim]. this will be true if the last time we
+ // split on this dimension, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+ // scratch value to return from getPackedValue:
+ private final BytesRef scratch;
+
+ PackedIndexTree()
+ {
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[packedBytesLength];
+ readNodeData(false);
+ scratch = new BytesRef();
+ scratch.length = bytesPerValue;
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
Review Comment:
nit: Here and in `pushRight()`, decent `assert` messages?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227896800
##########
src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java:
##########
@@ -130,6 +131,23 @@ public static ByteComparable max(ByteComparable a, ByteComparable b)
return a == null ? b : (b == null || ByteComparable.compare(b, a, ByteComparable.Version.OSS50) < 0) ? a : b;
}
+ /**
+ * Returns the value length for the given {@link AbstractType}, selecting 16 for types
+ * that officially use VARIABLE_LENGTH but are, in fact, of a fixed length.
+ */
+ public static int fixedSizeOf(AbstractType<?> type)
Review Comment:
We had this discussion previously and I said that I would look at adding this change to this PR. I did spend some time looking at this and came to the conclusion that this needed a separate ticket (as you noted). If have raised [CASSANDRA-18590](https://issues.apache.org/jira/browse/CASSANDRA-18590) for this as part of phase 2.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227912226
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDWriter.java:
##########
@@ -0,0 +1,782 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.LongBitSet;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension.
+ * <p>
+ * Recursively builds a block KD-tree to assign all incoming points in N-dim space to smaller
+ * and smaller N-dim rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * See <a href="https://www.cs.duke.edu/~pankaj/publications/papers/bkd-sstd.pdf">this paper</a> for details.
+ *
+ * <p>This consumes heap during writing: it allocates a <code>LongBitSet(numPoints)</code>,
+ * and then uses up to the specified {@code maxMBSortInHeap} heap space for writing.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ *
+ * lucene.experimental
+ */
+public class BKDWriter
Review Comment:
I would prefer `BlockBalancedTree` as a general descriptive name for this structure, but `blockbalancedtree` is a bit of a mouthful for a package name, or is it? The alternative would be to leave the classes using `BlockBalancedTree` and use `bbtree` for the package. WDYT?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228649027
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
Review Comment:
Any reason we don't just throw `UnsupportedOperationException` here and in `getDocID()`?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228785181
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
Review Comment:
```suggestion
private static class LeafCallback implements BlockBalancedTreeWriter.Callback
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227246720
##########
src/java/org/apache/cassandra/db/ClusteringComparator.java:
##########
@@ -353,6 +353,8 @@ public <V> Clustering<V> clusteringFromByteComparable(ValueAccessor<V> accessor,
{
ByteComparable.Version version = ByteComparable.Version.OSS50;
ByteSource.Peekable orderedBytes = ByteSource.peekable(comparable.asComparableBytes(version));
+ if (orderedBytes == null)
+ return null;
Review Comment:
note to self: Figure out when `orderedBytes` is `null` here...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228640460
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
+ }
+
+ @Override
+ public int getDocID(int i)
+ {
+ return 0;
+ }
+
+ @Override
+ public void swap(int i, int j)
+ {
+ throw new IllegalStateException("unexpected sorting");
+ }
+
+ @Override
+ public void intersect(PointValues.IntersectVisitor visitor)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long estimatePointCount(PointValues.IntersectVisitor visitor)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public byte[] getMinPackedValue()
+ {
+ return EMPTY;
+ }
+
+ @Override
+ public byte[] getMaxPackedValue()
+ {
+ return EMPTY;
+ }
+
+ @Override
+ public int getNumDimensions()
+ {
+ return 1;
+ }
+
+ @Override
+ public int getBytesPerDimension()
+ {
+ return 0;
+ }
+
+ public interface IntersectVisitor
+ {
+ /**
+ * Called for all documents in a leaf cell that crosses the query. The consumer should scrutinize the
+ * packedValue to decide whether to accept it. Values are visited in increasing order, and in the case of ties,
+ * in increasing docID order.
Review Comment:
nit: Perhaps a place where we could move from `docID` -> `rowID`? (here and in the method signature?)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228807324
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
Review Comment:
```suggestion
reader.intersect((rowID, packedValue) -> oneDimWriter.add(packedValue, rowID));
```
?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228249036
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible
+ * tree levels.
+ *
+ * Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
Review Comment:
I'd prefer to keep the low-level implementation and usage naming separate. For the time being anyway. There is a possibility that we will end up using the balanced tree for non-numeric types in the future.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228239378
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
Review Comment:
Agreed. I ended up getting rid of both of these and just using `packedValue.length`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229350978
##########
src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java:
##########
@@ -110,7 +112,7 @@ public void complete(Stopwatch stopwatch) throws IOException
try (MemtableTermsIterator terms = new MemtableTermsIterator(memtable.getMinTerm(), memtable.getMaxTerm(), iterator))
{
- long cellCount = flush(minKey, maxKey, indexContext.getValidator(), terms);
+ long cellCount = flush(minKey, maxKey, indexContext.getValidator(), terms, rowMapping.maxSSTableRowId);
Review Comment:
I've changed the `flush` method to take `maxSSTableRowId` because the `MemtableIndexWriter` is not really aware of segments as it only ever writes one. This value is passed to the `NumericIndexWriter` who interprets it as a `maxSegmentRowId` and we call `writeCompleteSegment` but I'm happy with that.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229587905
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class OneDimensionBKDWriter
Review Comment:
I've called this `TreeWriter` because it is responsible for writing part of the tree to disk.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229744563
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class OneDimensionBKDWriter
+ {
+ private final IndexOutput out;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafDocs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafCount;
+ private long lastDocID;
+
+ OneDimensionBKDWriter(IndexOutput out, Callback callback)
+ {
+ this.out = out;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafCount, lastPackedValue, packedValue, 0, docID, lastDocID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafCount * bytesPerValue, bytesPerValue);
+ leafDocs[leafCount] = docID;
+ 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();
+ leafCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastDocID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ public long finish() throws IOException
+ {
+ if (leafCount > 0)
+ {
+ writeLeafBlock();
+ leafCount = 0;
+ }
+
+ if (valueCount == 0)
+ {
+ return -1;
+ }
+
+ pointCount = valueCount;
+
+ long indexFP = out.getFilePointer();
+
+ int numInnerNodes = leafBlockStartValues.size();
+
+ byte[] index = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, index, leafBlockStartValues);
+ long[] arr = new long[leafBlockFilePointer.size()];
+ for (int i = 0; i < leafBlockFilePointer.size(); i++)
+ {
+ arr[i] = leafBlockFilePointer.get(i);
+ }
+ writeIndex(out, maxPointsInLeafNode, arr, index);
+ return indexFP;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(out.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
+ int prefix = bytesPerValue;
+ int offset = (leafCount - 1) * bytesPerValue;
+ for (int j = 0; j < bytesPerValue; j++)
+ {
+ if (leafValues[j] != leafValues[offset + j])
+ {
+ prefix = j;
+ break;
+ }
+ }
+
+ int commonPrefixLength = prefix;
+
+ assert scratchOut.getPosition() == 0;
+
+ out.writeVInt(leafCount);
+
+ for (int x = 0; x < leafCount; x++)
+ {
+ rowIDAndIndexes[x].valueOrderIndex = x;
+ rowIDAndIndexes[x].rowID = leafDocs[x];
+ }
+
+ final Sorter sorter = new IntroSorter()
+ {
+ RowIDAndIndex pivot;
+
+ @Override
+ protected void swap(int i, int j)
+ {
+ RowIDAndIndex o = rowIDAndIndexes[i];
+ rowIDAndIndexes[i] = rowIDAndIndexes[j];
+ rowIDAndIndexes[j] = o;
+ }
+
+ @Override
+ protected void setPivot(int i)
+ {
+ pivot = rowIDAndIndexes[i];
+ }
+
+ @Override
+ protected int comparePivot(int j)
+ {
+ return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID);
+ }
+ };
+
+ sorter.sort(0, leafCount);
+
+ // write leaf rowID -> orig index
+ scratchOut2.reset();
+
+ // iterate in row ID order to get the row ID index for the given value order index
+ // place into an array to be written as packed ints
+ for (int x = 0; x < leafCount; x++)
+ {
+ final int valueOrderIndex = rowIDAndIndexes[x].valueOrderIndex;
+ orderIndex[valueOrderIndex] = x;
+ }
+
+ LeafOrderMap.write(orderIndex, leafCount, maxPointsInLeafNode - 1, scratchOut2);
+
+ out.writeVInt(scratchOut2.getPosition());
+ out.writeBytes(scratchOut2.getBytes(), 0, scratchOut2.getPosition());
+
+ if (callback != null) callback.writeLeafDocs(rowIDAndIndexes, 0, leafCount);
+
+ writeCommonPrefixLength(scratchOut, commonPrefixLength, leafValues);
+
+ scratchBytesRef1.length = bytesPerValue;
+ scratchBytesRef1.bytes = leafValues;
+
+ IntFunction<BytesRef> packedValues = (i) -> {
+ scratchBytesRef1.offset = bytesPerValue * i;
+ return scratchBytesRef1;
+ };
+ if (DEBUG)
+ valuesInOrderAndBounds(leafCount,
+ ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue),
+ ArrayUtil.copyOfSubArray(leafValues, (leafCount - 1) * bytesPerValue, leafCount * bytesPerValue),
+ packedValues,
+ leafDocs);
+
+ writeLeafBlockPackedValues(scratchOut, commonPrefixLength, leafCount, packedValues);
+
+ out.writeBytes(scratchOut.getBytes(), 0, scratchOut.getPosition());
+
+ scratchOut.reset();
+ }
+ }
+
+ // TODO: there must be a simpler way?
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ private void checkMaxLeafNodeCount(int numLeaves)
+ {
+ if ((1 + bytesPerValue) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH)
+ {
+ throw new IllegalStateException("too many nodes; increase maxPointsInLeafNode (currently " + maxPointsInLeafNode + ") and reindex");
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index not fully balanced binary tree (only happens
+ // if it was created by OneDimensionBKDWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+ writeIndex(out, countPerLeaf, leafBlockFPs.length, packedIndex);
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, int numLeaves, byte[] packedIndex) throws IOException
+ {
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert numLeaves > 0;
+ out.writeVInt(numLeaves);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void writeLeafBlockPackedValues(DataOutput out, int commonPrefixLength, int count, IntFunction<BytesRef> packedValues) throws IOException
+ {
+ if (commonPrefixLength == bytesPerValue)
+ {
+ // all values in this block are equal
+ out.writeByte((byte) -1);
+ }
+ else
+ {
+ assert commonPrefixLength < bytesPerValue;
+ out.writeByte((byte) 0);
+ int compressedByteOffset = commonPrefixLength;
+ commonPrefixLength++;
+ 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, commonPrefixLength, i, i + runLen, packedValues);
+ i += runLen;
+ assert i <= count;
+ }
+ }
+ }
+
+
+ private void writeLeafBlockPackedValuesRange(DataOutput out, int commonPrefixLength, int start, int end, IntFunction<BytesRef> packedValues) throws IOException
+ {
+ for (int i = start; i < end; ++i)
+ {
+ BytesRef ref = packedValues.apply(i);
+ assert ref.length == bytesPerValue;
+
+ out.writeBytes(ref.bytes, ref.offset + commonPrefixLength, bytesPerValue - commonPrefixLength);
+ }
+ }
+
+ private static int runLen(IntFunction<BytesRef> packedValues, int start, int end, int byteOffset)
+ {
+ BytesRef first = packedValues.apply(start);
+ byte b = first.bytes[first.offset + byteOffset];
+ for (int i = start + 1; i < end; ++i)
+ {
+ BytesRef ref = packedValues.apply(i);
+ byte b2 = ref.bytes[ref.offset + byteOffset];
+ assert Byte.toUnsignedInt(b2) >= Byte.toUnsignedInt(b);
+ if (b != b2)
+ {
+ return i - start;
+ }
+ }
+ return end - start;
+ }
+
+ private void writeCommonPrefixLength(DataOutput out, int commonPrefixLength, byte[] packedValue) throws IOException
+ {
+ out.writeVInt(commonPrefixLength);
+ if (commonPrefixLength > 0)
+ out.writeBytes(packedValue, 0, commonPrefixLength);
+ }
+
+ // The following 3 methods are only used when DEBUG is true:
+
+ private void valueInBounds(BytesRef packedValue, byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ if (FutureArrays.compareUnsigned(packedValue.bytes,
+ packedValue.offset,
+ packedValue.offset + bytesPerValue,
+ minPackedValue,
+ 0,
+ bytesPerValue) < 0)
+ {
+ throw new AssertionError("value=" + new BytesRef(packedValue.bytes, packedValue.offset, bytesPerValue) +
+ " is < minPackedValue=" + new BytesRef(minPackedValue));
+ }
+
+ if (FutureArrays.compareUnsigned(packedValue.bytes,
+ packedValue.offset,
+ packedValue.offset + bytesPerValue,
+ maxPackedValue, 0,
+ bytesPerValue) > 0)
+ {
+ throw new AssertionError("value=" + new BytesRef(packedValue.bytes, packedValue.offset, bytesPerValue) +
+ " is > maxPackedValue=" + new BytesRef(maxPackedValue));
+ }
+ }
+
+ private void valuesInOrderAndBounds(int count, byte[] minPackedValue, byte[] maxPackedValue, IntFunction<BytesRef> values, long[] docs)
+ {
+ byte[] lastPackedValue = new byte[bytesPerValue];
+ long lastDoc = -1;
+ for (int i = 0; i < count; i++)
+ {
+ BytesRef packedValue = values.apply(i);
+ assert packedValue.length == bytesPerValue;
+ valueInOrder(i, lastPackedValue, packedValue.bytes, packedValue.offset, docs[i], lastDoc);
+ lastDoc = docs[i];
+
+ // Make sure this value does in fact fall within this leaf cell:
+ valueInBounds(packedValue, minPackedValue, maxPackedValue);
+ }
+ }
+
+ private void valueInOrder(long ord, byte[] lastPackedValue, byte[] packedValue, int packedValueOffset, long doc, long lastDoc)
+ {
+ int dimOffset = 0;
+ if (ord > 0)
+ {
+ int cmp = FutureArrays.compareUnsigned(lastPackedValue, dimOffset, dimOffset + bytesPerValue, packedValue, packedValueOffset + dimOffset, packedValueOffset + dimOffset + bytesPerValue);
+ if (cmp > 0)
+ {
+ throw new AssertionError("values out of order: last value=" + new BytesRef(lastPackedValue) +
+ " current value=" + new BytesRef(packedValue, packedValueOffset, bytesPerValue) +
+ " ord=" + ord);
+ }
+ if (cmp == 0 && doc < lastDoc)
+ {
+ throw new AssertionError("docs out of order: last doc=" + lastDoc + " current doc=" + doc + " ord=" + ord);
+ }
+ }
+ System.arraycopy(packedValue, packedValueOffset, lastPackedValue, 0, bytesPerValue);
+ }
+}
Review Comment:
Agreed. I have also done a little reorganisation of the whole class so that it's easier to read. I've moved the inner interface and classes to the bottom of the class with the `TreeWriter` owning its own methods. I've changed the behaviour of the `TreeWriter.finish` method so it now returns the total point count with the `BlockBalancedTreeWriter.writeTree` method assuming responsibility for fetching the file pointer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1231760277
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
Review Comment:
Note: We only write the min value on the first block, and subsequently overwrite the max on every block.
(No action necessary on these...just making notes to myself that may or may not become inline comments...)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229412341
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
Review Comment:
I've changed all the methods that are returning dummy values to return `UnsupportedOperationException`. These methods are only used in the sort operation and that is only done if `size() > 0`. This is only the case for the implementation in `BlockBalancedTreeRamBuffer` where the values need sorting.
On a related subject. It seems to me that the only reason that we are using `PointValues` at all is because of the sort. This is needed because the values in `BlockBalancedTreeRamBuffer` are not sorted. One option here would be to change how we build the in-memory numerics such that they are naturally sorted. I am thinking of a trie here. This would also give us prefix compression of the values as they are built in-memory, which we don't have with the current implementation.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230136041
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
Review Comment:
Yeah. I guess it doesn't have to be a super accurate estimate...just enough to avoid silly `grow()` calls.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232951635
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+
+public class BlockBalancedTreeQueries
+{
+ private static final BlockBalancedTreeReader.IntersectVisitor MATCH_ALL = new BlockBalancedTreeReader.IntersectVisitor()
+ {
+ @Override
+ public boolean visit(byte[] packedValue)
+ {
+ return true;
+ }
+
+ @Override
+ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ return Relation.CELL_INSIDE_QUERY;
+ }
+ };
+
+ public static BlockBalancedTreeReader.IntersectVisitor balancedTreeQueryFrom(Expression expression, int bytesPerValue)
+ {
+ if (expression.lower == null && expression.upper == null)
+ {
+ return MATCH_ALL;
+ }
+
+ Bound lower = null ;
+ if (expression.lower != null)
+ {
+ final byte[] lowerBound = toComparableBytes(bytesPerValue, expression.lower.value.encoded, expression.validator);
+ lower = new Bound(lowerBound, !expression.lower.inclusive);
+ }
+
+ Bound upper = null;
+ if (expression.upper != null)
+ {
+ final byte[] upperBound = toComparableBytes(bytesPerValue, expression.upper.value.encoded, expression.validator);
+ upper = new Bound(upperBound, !expression.upper.inclusive);
+ }
+
+ return new RangeQueryVisitor(bytesPerValue, lower, upper);
+ }
+
+ private static byte[] toComparableBytes(int bytesPerDim, ByteBuffer value, AbstractType<?> type)
+ {
+ byte[] buffer = new byte[TypeUtil.fixedSizeOf(type)];
+ assert buffer.length == bytesPerDim;
+ TypeUtil.toComparableBytes(value, type, buffer);
+ return buffer;
+ }
+
+ private static abstract class RangeQuery implements BlockBalancedTreeReader.IntersectVisitor
+ {
+ final int bytesPerValue;
+
+ RangeQuery(int bytesPerValue)
+ {
+ this.bytesPerValue = bytesPerValue;
+ }
+
+ int compareUnsigned(byte[] packedValue, Bound bound)
+ {
+ return FutureArrays.compareUnsigned(packedValue, 0, bytesPerValue, bound.bound, 0, bytesPerValue);
+ }
+ }
+
+ private static class Bound
+ {
+ private final byte[] bound;
+ private final boolean exclusive;
+
+ Bound(byte[] bound, boolean exclusive)
+ {
+ this.bound = bound;
+ this.exclusive = exclusive;
+ }
+
+ boolean smallerThan(int cmp)
+ {
+ return cmp > 0 || (cmp == 0 && exclusive);
+ }
+
+ boolean greaterThan(int cmp)
+ {
+ return cmp < 0 || (cmp == 0 && exclusive);
+ }
+ }
+
+ private static class RangeQueryVisitor extends RangeQuery
+ {
+ private final Bound lower;
+ private final Bound upper;
+
+ private RangeQueryVisitor(int bytesPerValue, Bound lower, Bound upper)
+ {
+ super(bytesPerValue);
+ this.lower = lower;
+ this.upper = upper;
+ }
+
+ @Override
+ public boolean visit(byte[] packedValue)
+ {
+ if (lower != null)
+ {
+ if (lower.greaterThan(compareUnsigned(packedValue, lower)))
+ {
+ // value is too low, in this dimension
+ return false;
+ }
+ }
+
+ if (upper != null)
+ {
+ return !upper.smallerThan(compareUnsigned(packedValue, upper));
+ }
+
+ return true;
+ }
+
+ @Override
+ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ boolean crosses = false;
+
+ if (lower != null)
+ {
+ int maxCmp = compareUnsigned(maxPackedValue, lower);
+ if (lower.greaterThan(maxCmp))
+ return Relation.CELL_OUTSIDE_QUERY;
+
+ int minCmp = compareUnsigned(minPackedValue, lower);
+ crosses = lower.greaterThan(minCmp);
+ }
+
+ if (upper != null)
+ {
+ int minCmp = compareUnsigned(minPackedValue, upper);
+ if (upper.smallerThan(minCmp))
+ return Relation.CELL_OUTSIDE_QUERY;
+
+ int maxCmp = compareUnsigned(maxPackedValue, upper);
+ crosses |= upper.smallerThan(maxCmp);
+ }
+
+ if (crosses)
+ {
+ return Relation.CELL_CROSSES_QUERY;
+ }
+ else
+ {
+ return Relation.CELL_INSIDE_QUERY;
+ }
Review Comment:
nit: `return crosses ? Relation.CELL_CROSSES_QUERY : Relation.CELL_INSIDE_QUERY;`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239671714
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
Review Comment:
We could look at packing the values for trie postings but even if we didn't I still think we would gain more from the trie prefix compression than we would lose from the `PackedLongValues`. I'm also looking at the `RAMStringIndexer` here. We could replace that with exactly the same in-memory structure. Whether it gave us much of an improvement or not, it would reduce code complexity and reduce our reliance on Lucene code.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1244005643
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+
+ in.readBytes(minPackedValue, 0, bytesPerValue);
+ in.readBytes(maxPackedValue, 0, bytesPerValue);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(packedIndex, bytesPerValue, numLeaves), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final static class PackedIndexTree
+ {
+ private final int bytesPerValue;
+ private final int numLeaves;
+
+ private final byte[][] splitPackedValueStack;
+ // used to read the packed index byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the delta we read for the node at this level is a negative offset vs. the last split;
+ // this will be true if the last time we split, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ PackedIndexTree(byte[] packedIndex, int bytesPerValue, int numLeaves)
+ {
+ this.bytesPerValue = bytesPerValue;
+ this.numLeaves = numLeaves;
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[bytesPerValue];
+ readNodeData(false);
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = true;
+ in.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = false;
+ in.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ leafNode = false;
+ }
+
+ public boolean isLeafNode()
+ {
+ return nodeID >= numLeaves;
+ }
+
+ public boolean nodeExists()
+ {
+ return nodeID - numLeaves < numLeaves;
+ }
+
+ public int getNodeID()
+ {
+ return nodeID;
+ }
+
+ public byte[] getSplitPackedValue()
+ {
+ assert !isLeafNode();
+ assert splitPackedValueStack[level] != null : "level=" + level;
+ return splitPackedValueStack[level];
+ }
+
+ public long getLeafBlockFP()
+ {
+ assert isLeafNode() : "nodeID=" + nodeID + " is not a leaf";
+ return leafBlockFPStack[level];
+ }
+
+ public byte[] getSplitValue()
+ {
+ assert !isLeafNode();
+ return splitValuesStack[level];
+ }
+
+ private void readNodeData(boolean isLeft)
+ {
+ leafBlockFPStack[level] = leafBlockFPStack[level - 1];
+
+ // read leaf block FP delta
+ if (!isLeft)
+ leafBlockFPStack[level] += in.readVLong();
+
+ leafNode = isLeafNode();
+ if (!leafNode)
+ {
+ // read prefix, firstDiffByteDelta encoded as int:
+ int code = in.readVInt();
+ int prefix = code % (1 + bytesPerValue);
+ int suffix = bytesPerValue - prefix;
+
+ if (splitValuesStack[level] == null)
+ {
+ splitValuesStack[level] = new byte[bytesPerValue];
+ }
+ System.arraycopy(splitValuesStack[level - 1], 0, splitValuesStack[level], 0, bytesPerValue);
+ if (suffix > 0)
+ {
+ int firstDiffByteDelta = code / (1 + bytesPerValue);
+ if (negativeDeltas[level])
+ firstDiffByteDelta = -firstDiffByteDelta;
+ int oldByte = splitValuesStack[level][prefix] & 0xFF;
+ splitValuesStack[level][prefix] = (byte) (oldByte + firstDiffByteDelta);
+ in.readBytes(splitValuesStack[level], prefix + 1, suffix - 1);
+ }
+
+ int leftNumBytes = nodeID * 2 < numLeaves ? in.readVInt() : 0;
+
+ leftNodePositions[level] = in.getPosition();
+ rightNodePositions[level] = leftNodePositions[level] + leftNumBytes;
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
Review Comment:
Firstly, neither of the above suggestions work correctly because they underestimate the tree depth for low values of `numLeaves`. Secondly, I don't believe that this method was ever meant to be 100% accurate for the tree depth, it returns a value that is as close as possible but will have a tendency to overestimate.
If we can come up with a method that is 100% accurate then I am more than happy to change the method but for the time being I'd prefer to leave as is.
I am thinking that we could just add the tree depth to the on-disk format because the write side must know the value.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245770385
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+ private final long maxRows;
+
+ public BlockBalancedTreeWriter(long maxRows, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxRows = maxRows;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxRows), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> leafWriter.add(packedValue, rowID));
+
+ valueCount = leafWriter.finish();
+
+ long treeFilePointer = valueCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ // There is only any point in writing the balanced tree if any values were added
+ if (treeFilePointer >= 0)
+ writeBalancedTree(treeOutput, maxPointsInLeafNode, leafWriter.leafBlockStartValues, leafWriter.leafBlockFilePointers);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return treeFilePointer;
+ }
+
+ private void writeBalancedTree(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitValues = new byte[(1 + numInnerNodes) * bytesPerValue];
+ int depth = recurseBalanceTree(1, 0, numInnerNodes, 1, splitValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ out.writeVInt(leafBlockFPs.length);
+ out.writeVInt(Math.min(depth, leafBlockFPs.length));
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(valueCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ /**
+ * This can, potentially, be removed in the future by CASSANDRA-18597
+ */
+ private int recurseBalanceTree(int nodeID, int offset, int count, int treeDepth, byte[] splitValues, List<byte[]> leafBlockStartValues)
+ {
+ treeDepth++;
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ int leftTreeDepth = recurseBalanceTree(2 * nodeID, offset, leftHalf, treeDepth, splitValues, leafBlockStartValues);
+
+ // Recurse right
+ int rightTreeDepth = recurseBalanceTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, treeDepth, splitValues, leafBlockStartValues);
+ return Math.max(leftTreeDepth, rightTreeDepth);
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ return treeDepth;
+ }
+
+ // Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure.
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValue = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitValues, 0, blocks, 1, lastSplitValue, false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValue is the split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValue, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ throw new IllegalStateException();
+// return 0;
Review Comment:
TODO: cleanup, etc.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson closed pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson closed pull request #2409: Numeric on-disk index write and search
URL: https://github.com/apache/cassandra/pull/2409
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245397093
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+
+ in.readBytes(minPackedValue, 0, bytesPerValue);
+ in.readBytes(maxPackedValue, 0, bytesPerValue);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(packedIndex, bytesPerValue, numLeaves), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final static class PackedIndexTree
+ {
+ private final int bytesPerValue;
+ private final int numLeaves;
+
+ private final byte[][] splitPackedValueStack;
+ // used to read the packed index byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the delta we read for the node at this level is a negative offset vs. the last split;
+ // this will be true if the last time we split, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ PackedIndexTree(byte[] packedIndex, int bytesPerValue, int numLeaves)
+ {
+ this.bytesPerValue = bytesPerValue;
+ this.numLeaves = numLeaves;
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[bytesPerValue];
+ readNodeData(false);
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = true;
+ in.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = false;
+ in.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ leafNode = false;
+ }
+
+ public boolean isLeafNode()
+ {
+ return nodeID >= numLeaves;
+ }
+
+ public boolean nodeExists()
+ {
+ return nodeID - numLeaves < numLeaves;
+ }
+
+ public int getNodeID()
+ {
+ return nodeID;
+ }
+
+ public byte[] getSplitPackedValue()
+ {
+ assert !isLeafNode();
+ assert splitPackedValueStack[level] != null : "level=" + level;
+ return splitPackedValueStack[level];
+ }
+
+ public long getLeafBlockFP()
+ {
+ assert isLeafNode() : "nodeID=" + nodeID + " is not a leaf";
+ return leafBlockFPStack[level];
+ }
+
+ public byte[] getSplitValue()
+ {
+ assert !isLeafNode();
+ return splitValuesStack[level];
+ }
+
+ private void readNodeData(boolean isLeft)
+ {
+ leafBlockFPStack[level] = leafBlockFPStack[level - 1];
+
+ // read leaf block FP delta
+ if (!isLeft)
+ leafBlockFPStack[level] += in.readVLong();
+
+ leafNode = isLeafNode();
+ if (!leafNode)
+ {
+ // read prefix, firstDiffByteDelta encoded as int:
+ int code = in.readVInt();
+ int prefix = code % (1 + bytesPerValue);
+ int suffix = bytesPerValue - prefix;
+
+ if (splitValuesStack[level] == null)
+ {
+ splitValuesStack[level] = new byte[bytesPerValue];
+ }
+ System.arraycopy(splitValuesStack[level - 1], 0, splitValuesStack[level], 0, bytesPerValue);
+ if (suffix > 0)
+ {
+ int firstDiffByteDelta = code / (1 + bytesPerValue);
+ if (negativeDeltas[level])
+ firstDiffByteDelta = -firstDiffByteDelta;
+ int oldByte = splitValuesStack[level][prefix] & 0xFF;
+ splitValuesStack[level][prefix] = (byte) (oldByte + firstDiffByteDelta);
+ in.readBytes(splitValuesStack[level], prefix + 1, suffix - 1);
+ }
+
+ int leftNumBytes = nodeID * 2 < numLeaves ? in.readVInt() : 0;
+
+ leftNodePositions[level] = in.getPosition();
+ rightNodePositions[level] = leftNodePositions[level] + leftNumBytes;
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
Review Comment:
I have ended up adding the `treeDepth` to the packed index and removing this method.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239701440
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
Review Comment:
Yes, and I have tried to reach this point in testing. I will keep looking at this but it I have yet to determine what it means by an unbalanced tree.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227229860
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDReader.java:
##########
@@ -0,0 +1,470 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a multidimensional shape in byte[] space with a block KD-tree previously written with
+ * {@link BKDWriter}.
Review Comment:
https://github.com/apache/cassandra/pull/2409/files#r1227216477
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227306295
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
+
+ docIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(docIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ if (value.length != packedBytesLength)
+ {
+ throw new IllegalArgumentException("The value has length=" + value.length + " but should be " + pointNumBytes);
+ }
+
+ long startingBytesUsed = bytesUsed.get();
+ long startingDocIDsBytesUsed = docIDsBuilder.ramBytesUsed();
+
+ docIDsBuilder.add(segmentRowId);
+ bytes.append(value);
+
+ if (segmentRowId != lastSegmentRowID)
+ {
+ numRows++;
+ lastSegmentRowID = segmentRowId;
+ }
+
+ numPoints++;
+
+ long docIDsAllocatedBytes = docIDsBuilder.ramBytesUsed() - startingDocIDsBytesUsed;
+ long endingBytesAllocated = bytesUsed.addAndGet(docIDsAllocatedBytes);
+
+ return endingBytesAllocated - startingBytesUsed;
+ }
+
+ public MutableOneDimPointValues asPointValues()
+ {
+ ensureOpen();
+ // building packed longs is destructive
+ closed = true;
+ final PackedLongValues docIDs = docIDsBuilder.build();
+ return new MutableOneDimPointValues()
+ {
+ final int[] ords = new int[numPoints];
+
+ {
+ for (int i = 0; i < numPoints; ++i)
+ {
+ ords[i] = i;
+ }
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ final long offset = (long) packedBytesLength * (long) ords[i];
+ packedValue.length = packedBytesLength;
+ bytes.setRawBytesRef(packedValue, offset);
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ final long offset = (long) packedBytesLength * (long) ords[i] + (long) k;
+
+ return bytes.readByte(offset);
+ }
+
+ @Override
+ public int getDocID(int i)
+ {
+ return Math.toIntExact(docIDs.get(ords[i]));
+ }
+
+ @Override
+ public void swap(int i, int j)
+ {
+ int tmp = ords[i];
+ ords[i] = ords[j];
+ ords[j] = tmp;
+ }
+
+ @Override
+ public void intersect(IntersectVisitor visitor) throws IOException
Review Comment:
nit: Should `intersect()` be `visit()` or `visitAll()` or something along those lines?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1229586092
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
Review Comment:
I'll go with `writeTree` because it's what it's doing.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227305235
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
+
+ docIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(docIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ if (value.length != packedBytesLength)
+ {
+ throw new IllegalArgumentException("The value has length=" + value.length + " but should be " + pointNumBytes);
+ }
+
+ long startingBytesUsed = bytesUsed.get();
+ long startingDocIDsBytesUsed = docIDsBuilder.ramBytesUsed();
+
+ docIDsBuilder.add(segmentRowId);
+ bytes.append(value);
+
+ if (segmentRowId != lastSegmentRowID)
+ {
+ numRows++;
+ lastSegmentRowID = segmentRowId;
+ }
+
+ numPoints++;
+
+ long docIDsAllocatedBytes = docIDsBuilder.ramBytesUsed() - startingDocIDsBytesUsed;
+ long endingBytesAllocated = bytesUsed.addAndGet(docIDsAllocatedBytes);
+
+ return endingBytesAllocated - startingBytesUsed;
+ }
+
+ public MutableOneDimPointValues asPointValues()
+ {
+ ensureOpen();
+ // building packed longs is destructive
+ closed = true;
+ final PackedLongValues docIDs = docIDsBuilder.build();
+ return new MutableOneDimPointValues()
+ {
+ final int[] ords = new int[numPoints];
Review Comment:
nit: Not a hot path, so could try to on-liner this...
```
final int[] ords = IntStream.range(0, numPoints).toArray();
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227287150
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDTreeRamBuffer.java:
##########
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link MutablePointValues}.
+ */
+public class BKDTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final int pointNumBytes;
+ private final int packedBytesLength;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder docIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BKDTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+ this.pointNumBytes = pointNumBytes;
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+ packedBytesLength = pointNumBytes;
+
+ docIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(docIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ if (value.length != packedBytesLength)
+ {
+ throw new IllegalArgumentException("The value has length=" + value.length + " but should be " + pointNumBytes);
+ }
Review Comment:
Should this just be an assertion (w/ the helpful message)? It's not like we have 3rd party users, and hitting it would be a clear programmer error rather than usage error...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228444058
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
Review Comment:
nit: Could use `@see` here to point to `BKDWriter`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228445739
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,332 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ *
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(IndexTreeTraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle indexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int leafNodeOffset;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle indexFile, long root)
+ {
+ this.indexFile = indexFile;
+
+ try (RandomAccessReader reader = indexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(root);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+ leafNodeOffset = numLeaves;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(indexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(indexFile);
+ }
+
+ void traverse(IndexTreeTraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(IndexTreeTraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ /**
+ * Copy of BKDReader#getTreeDepth()
Review Comment:
```suggestion
* Copy of {@link BKDReader#getTreeDepth()}
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228453597
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.stream.IntStream;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.ByteBlockPool;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Counter;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for point values that provides a sortable view of itself as {@link IntersectingPointValues}.
+ */
+public class BlockBalancedTreeRamBuffer implements Accountable
+{
+ private final Counter bytesUsed;
+ private final ByteBlockPool bytes;
+ private final byte[] packedValue;
+ private final PackedLongValues.Builder rowIDsBuilder;
+ private int numPoints;
+ private int numRows;
+ private int lastSegmentRowID = -1;
+ private boolean closed = false;
+
+ public BlockBalancedTreeRamBuffer(int pointNumBytes)
+ {
+ this.bytesUsed = Counter.newCounter();
+
+ this.bytes = new ByteBlockPool(new ByteBlockPool.DirectTrackingAllocator(bytesUsed));
+
+ packedValue = new byte[pointNumBytes];
+
+ rowIDsBuilder = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
+ bytesUsed.addAndGet(rowIDsBuilder.ramBytesUsed());
+ }
+
+ @Override
+ public long ramBytesUsed()
+ {
+ return bytesUsed.get();
+ }
+
+ public int numRows()
+ {
+ return numRows;
+ }
+
+ public long addPackedValue(int segmentRowId, BytesRef value)
+ {
+ ensureOpen();
+
+ assert value.length == packedValue.length : "The value has length=" + value.length + " but should be " + packedValue.length;
+
+ long startingBytesUsed = bytesUsed.get();
+ long startingRowIDsBytesUsed = rowIDsBuilder.ramBytesUsed();
+
+ rowIDsBuilder.add(segmentRowId);
+ bytes.append(value);
+
+ if (segmentRowId != lastSegmentRowID)
Review Comment:
Is there ever a case where we would actually add values for duplicate rows, one after the other? If not, this becomes an assertion at most?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228973163
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class OneDimensionBKDWriter
+ {
+ private final IndexOutput out;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafDocs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafCount;
Review Comment:
```suggestion
private int valuesInLeaf;
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227961896
##########
src/java/org/apache/cassandra/index/sai/disk/v1/kdtree/BKDWriter.java:
##########
@@ -0,0 +1,782 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.kdtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.LongBitSet;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension.
+ * <p>
+ * Recursively builds a block KD-tree to assign all incoming points in N-dim space to smaller
+ * and smaller N-dim rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * See <a href="https://www.cs.duke.edu/~pankaj/publications/papers/bkd-sstd.pdf">this paper</a> for details.
+ *
+ * <p>This consumes heap during writing: it allocates a <code>LongBitSet(numPoints)</code>,
+ * and then uses up to the specified {@code maxMBSortInHeap} heap space for writing.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ *
+ * lucene.experimental
+ */
+public class BKDWriter
Review Comment:
`BlockBalancedTree` and use `bbtree` for the package sounds good to me.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227964300
##########
src/java/org/apache/cassandra/index/sai/utils/TypeUtil.java:
##########
@@ -130,6 +131,23 @@ public static ByteComparable max(ByteComparable a, ByteComparable b)
return a == null ? b : (b == null || ByteComparable.compare(b, a, ByteComparable.Version.OSS50) < 0) ? a : b;
}
+ /**
+ * Returns the value length for the given {@link AbstractType}, selecting 16 for types
+ * that officially use VARIABLE_LENGTH but are, in fact, of a fixed length.
+ */
+ public static int fixedSizeOf(AbstractType<?> type)
Review Comment:
A separate ticket makes sense to me. After all, this ticket doesn't introduce the `TypeUtil` class.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230018984
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
+ {
+ long balancedTreePosition;
+ SegmentMetadata.ComponentMetadataMap components = new SegmentMetadata.ComponentMetadataMap();
+
+ LeafCallback leafCallback = new LeafCallback();
+
+ try (IndexOutput balancedTreeOutput = indexDescriptor.openPerIndexOutput(IndexComponent.BALANCED_TREE, indexContext, true))
+ {
+ // The SSTable balanced tree component file is opened in append mode, so our offset is the current file pointer.
+ long balancedTreeOffset = balancedTreeOutput.getFilePointer();
+
+ balancedTreePosition = writer.writeField(balancedTreeOutput, values, leafCallback);
+
+ // If the balancedTreePosition is less than 0 then we didn't write any values out
+ // and the index is empty
+ if (balancedTreePosition < 0)
+ return components;
+
+ long balancedTreeLength = balancedTreeOutput.getFilePointer() - balancedTreeOffset;
+
+ Map<String, String> attributes = new LinkedHashMap<>();
+ attributes.put("max_points_in_leaf_node", Integer.toString(writer.getMaxPointsInLeafNode()));
+ attributes.put("num_leaves", Integer.toString(leafCallback.numLeaves()));
+ attributes.put("num_points", Long.toString(writer.getPointCount()));
+ attributes.put("bytes_per_dim", Long.toString(writer.getBytesPerValue()));
Review Comment:
```suggestion
attributes.put("bytes_per_value", Long.toString(writer.getBytesPerValue()));
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230187587
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
Review Comment:
nit: Untested code path?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232831923
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BlockBalancedTreeWriter}). That allows us to skip reading the leaves, instead
+ * just order leaf blocks by their offset in the index file, and correlate them with buffered posting lists.
+ */
+public class BlockBalancedTreePostingsWriter implements TraversingBlockBalancedTreeReader.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
+ private final TreeMap<Long, Integer> leafOffsetToNodeID = new TreeMap<>(Long::compareTo);
+ private final Multimap<Integer, Integer> nodeToChildLeaves = HashMultimap.create();
+
+ private final IndexContext indexContext;
+ int numNonLeafPostings = 0;
+ int numLeafPostings = 0;
+
+ BlockBalancedTreePostingsWriter(List<PackedLongValues> postings, IndexContext indexContext)
+ {
+ this.postings = postings;
+ this.indexContext = indexContext;
+ }
+
+ @Override
+ public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot)
+ {
+ checkArgument(!pathToRoot.containsInt(leafNodeID));
+ checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1));
+
+ leafOffsetToNodeID.put(leafBlockFP, leafNodeID);
+ for (int i = 0; i < pathToRoot.size(); i++)
+ {
+ int level = i + 1;
+ if (isLevelEligibleForPostingList(level))
+ {
+ int nodeID = pathToRoot.get(i);
+ nodeToChildLeaves.put(nodeID, leafNodeID);
+ }
+ }
+ }
+
+ public long finish(IndexOutput out) throws IOException
+ {
+ checkState(postings.size() == leafOffsetToNodeID.size(),
+ "Expected equal number of postings lists (%s) and leaf offsets (%s).",
+ postings.size(), leafOffsetToNodeID.size());
+
+ try (PostingsWriter postingsWriter = new PostingsWriter(out))
+ {
+ Iterator<PackedLongValues> postingsIterator = postings.iterator();
+ Map<Integer, PackedLongValues> leafToPostings = new HashMap<>();
+ leafOffsetToNodeID.forEach((fp, nodeID) -> leafToPostings.put(nodeID, postingsIterator.next()));
+
+ long postingsRamBytesUsed = postings.stream()
+ .mapToLong(PackedLongValues::ramBytesUsed)
+ .sum();
+
+ List<Integer> internalNodeIDs = nodeToChildLeaves.keySet()
+ .stream()
+ .filter(i -> nodeToChildLeaves.get(i).size() >= MINIMUM_POSTINGS_LEAVES)
+ .collect(Collectors.toList());
+
+ Collection<Integer> leafNodeIDs = leafOffsetToNodeID.values();
+
+ logger.debug(indexContext.logMessage("Writing posting lists for {} internal and {} leaf balanced tree nodes. Leaf postings memory usage: {}."),
+ internalNodeIDs.size(),
+ leafNodeIDs.size(),
+ FBUtilities.prettyPrintMemory(postingsRamBytesUsed));
Review Comment:
nit: Honestly would be fine w/ these 3 arguments on one line. Helps me line them up w/ the `{}`s above.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232956529
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final class PackedIndexTree
Review Comment:
nit: Part of me wants to call this class like `TraversalState` or something that indicates it's evolving as we walk the tree...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232560225
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(treeOutput.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
+ int commonPrefixLength = bytesPerValue;
+ int offset = (leafValueCount - 1) * bytesPerValue;
+ for (int j = 0; j < bytesPerValue; j++)
+ {
+ if (leafValues[j] != leafValues[offset + j])
+ {
+ commonPrefixLength = j;
+ break;
+ }
+ }
+
+ assert scratchOut.getPosition() == 0;
+
+ treeOutput.writeVInt(leafValueCount);
+
+ for (int x = 0; x < leafValueCount; x++)
+ {
+ rowIDAndIndexes[x].valueOrderIndex = x;
+ rowIDAndIndexes[x].rowID = leafRowIDs[x];
+ }
+
+ final Sorter sorter = new IntroSorter()
+ {
+ RowIDAndIndex pivot;
+
+ @Override
+ protected void swap(int i, int j)
+ {
+ RowIDAndIndex o = rowIDAndIndexes[i];
+ rowIDAndIndexes[i] = rowIDAndIndexes[j];
+ rowIDAndIndexes[j] = o;
+ }
+
+ @Override
+ protected void setPivot(int i)
+ {
+ pivot = rowIDAndIndexes[i];
+ }
+
+ @Override
+ protected int comparePivot(int j)
+ {
+ return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID);
+ }
+ };
+
+ sorter.sort(0, leafValueCount);
+
+ // write leaf rowID -> orig index
+ scratchOut2.reset();
+
+ // iterate in row ID order to get the row ID index for the given value order index
+ // place into an array to be written as packed ints
+ for (int x = 0; x < leafValueCount; x++)
+ orderIndex[rowIDAndIndexes[x].valueOrderIndex] = x;
+
+ LeafOrderMap.write(orderIndex, leafValueCount, maxPointsInLeafNode - 1, scratchOut2);
+
+ treeOutput.writeVInt(scratchOut2.getPosition());
+ treeOutput.writeBytes(scratchOut2.getBytes(), 0, scratchOut2.getPosition());
+
+ callback.writeLeafDocs(rowIDAndIndexes, 0, leafValueCount);
+
+ writeCommonPrefix(scratchOut, commonPrefixLength, leafValues);
+
+ scratchBytesRef1.length = bytesPerValue;
+ scratchBytesRef1.bytes = leafValues;
+
+ IntFunction<BytesRef> packedValues = (i) -> {
+ scratchBytesRef1.offset = bytesPerValue * i;
+ return scratchBytesRef1;
+ };
+ if (DEBUG)
+ valuesInOrderAndBounds(leafValueCount,
+ ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue),
+ ArrayUtil.copyOfSubArray(leafValues, (leafValueCount - 1) * bytesPerValue, leafValueCount * bytesPerValue),
+ packedValues,
+ leafRowIDs);
+
+ writeLeafBlockPackedValues(scratchOut, commonPrefixLength, leafValueCount, packedValues);
+
+ treeOutput.writeBytes(scratchOut.getBytes(), 0, scratchOut.getPosition());
+
+ scratchOut.reset();
+ }
+
+ private void checkMaxLeafNodeCount(int numLeaves)
+ {
+ if ((1 + bytesPerValue) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH)
+ {
+ throw new IllegalStateException("too many nodes; increase maxPointsInLeafNode (currently " + maxPointsInLeafNode + ") and reindex");
Review Comment:
So increasing `maxPointsInLeafNode` would mean there were...fewer first leaf values?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239663023
##########
src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java:
##########
@@ -235,7 +235,7 @@ public int openFilesPerSSTableIndex()
@Override
public int openFilesPerColumnIndex(IndexContext indexContext)
{
- // For the V1 format there are always 2 open files per index - index (kdtree or terms) + postings
+ // For the V1 format there are always 2 open files per index - index (balanced tree or terms) + postings
Review Comment:
Correct. I will add a note to this effect.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1234551076
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final int packedBytesLength;
+
+ TraversingBlockBalancedTreeReader(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+ packedBytesLength = bytesPerValue;
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[packedBytesLength];
+ maxPackedValue = new byte[packedBytesLength];
+
+ in.readBytes(minPackedValue, 0, packedBytesLength);
+ in.readBytes(maxPackedValue, 0, packedBytesLength);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
+ return MathUtil.log(numLeaves, 2) + 2;
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final class PackedIndexTree
+ {
+ private final byte[][] splitPackedValueStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ // used to read the packed byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the per-dim delta we read for the node at this level is a negative offset vs. the last split on this dim; this is a packed
+ // 2D array, i.e. to access array[level][dim] you read from negativeDeltas[level*numDims+dim]. this will be true if the last time we
+ // split on this dimension, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+ // scratch value to return from getPackedValue:
+ private final BytesRef scratch;
+
+ PackedIndexTree()
+ {
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[packedBytesLength];
+ readNodeData(false);
+ scratch = new BytesRef();
+ scratch.length = bytesPerValue;
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = true;
+ in.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[packedBytesLength];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = false;
+ in.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ leafNode = false;
+ }
+
+ public boolean isLeafNode()
+ {
+ return nodeID >= numLeaves;
+ }
+
+ public boolean nodeExists()
+ {
+ return nodeID - numLeaves < numLeaves;
+ }
+
+ public int getNodeID()
+ {
+ return nodeID;
+ }
+
+ public byte[] getSplitPackedValue()
+ {
+ assert !isLeafNode();
+ assert splitPackedValueStack[level] != null : "level=" + level;
+ return splitPackedValueStack[level];
+ }
+
+ public long getLeafBlockFP()
+ {
+ assert isLeafNode() : "nodeID=" + nodeID + " is not a leaf";
+ return leafBlockFPStack[level];
+ }
+
+ public BytesRef getSplitDimValue()
+ {
+ assert !isLeafNode();
+ scratch.bytes = splitValuesStack[level];
+ scratch.offset = 0;
+ return scratch;
+ }
+
+ private void readNodeData(boolean isLeft)
+ {
+ leafBlockFPStack[level] = leafBlockFPStack[level - 1];
+
+ // read leaf block FP delta
+ if (!isLeft)
Review Comment:
The pointer is already in the right spot for left children?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1243903222
##########
src/java/org/apache/cassandra/index/sai/disk/format/IndexComponent.java:
##########
@@ -31,6 +31,12 @@
*/
META("Meta"),
+ /**
+ * Balanced tree written by {@code BlockBalancedTreeWriter} indexes mappings of term to one or more segment row IDs
+ * (segment row ID = SSTable row ID - segment row ID offset).
+ */
+ BALANCED_TREE("BalancedTree"),
Review Comment:
Agreed, I missed this completely. I'll make sure we have a test in place for this.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242135701
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/NumericIndexSegmentSearcher.java:
##########
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.segment;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+
+import com.google.common.base.MoreObjects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
+import org.apache.cassandra.index.sai.disk.v1.bbtree.BlockBalancedTreeReader;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.postings.PostingList;
+
+import static org.apache.cassandra.index.sai.disk.v1.bbtree.BlockBalancedTreeQueries.balancedTreeQueryFrom;
+
+/**
+ * Executes {@link Expression}s against the balanced tree for an individual index segment.
+ */
+public class NumericIndexSegmentSearcher extends IndexSegmentSearcher
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final BlockBalancedTreeReader treeReader;
+ private final QueryEventListener.BalancedTreeEventListener perColumnEventListener;
+
+ NumericIndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory,
+ PerColumnIndexFiles perIndexFiles,
+ SegmentMetadata segmentMetadata,
+ IndexContext indexContext) throws IOException
+ {
+ super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, indexContext);
+
+ final long treePosition = metadata.getIndexRoot(IndexComponent.BALANCED_TREE);
+ assert treePosition >= 0;
+ final long postingsPosition = metadata.getIndexRoot(IndexComponent.POSTING_LISTS);
+ assert postingsPosition >= 0;
Review Comment:
Agreed. The assertions are pointless because we can't continue after this.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242162605
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BlockBalancedTreeWriter}). That allows us to skip reading the leaves, instead
+ * just order leaf blocks by their offset in the index file, and correlate them with buffered posting lists.
+ */
+public class BlockBalancedTreePostingsWriter implements TraversingBlockBalancedTreeReader.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
Review Comment:
It doesn't make sense having this or the `indexContext` here anyway. They are both only used in `finish`. If we got rid of these we could get rid of the constructor as well.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242278051
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeQueries.java:
##########
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.TypeUtil;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+
+public class BlockBalancedTreeQueries
+{
+ private static final BlockBalancedTreeReader.IntersectVisitor MATCH_ALL = new BlockBalancedTreeReader.IntersectVisitor()
+ {
+ @Override
+ public boolean visit(byte[] packedValue)
+ {
+ return true;
+ }
+
+ @Override
+ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue)
+ {
+ return Relation.CELL_INSIDE_QUERY;
+ }
+ };
+
+ public static BlockBalancedTreeReader.IntersectVisitor balancedTreeQueryFrom(Expression expression, int bytesPerValue)
+ {
+ if (expression.lower == null && expression.upper == null)
+ {
+ return MATCH_ALL;
+ }
+
+ Bound lower = null ;
+ if (expression.lower != null)
+ {
+ final byte[] lowerBound = toComparableBytes(bytesPerValue, expression.lower.value.encoded, expression.validator);
+ lower = new Bound(lowerBound, !expression.lower.inclusive);
+ }
+
+ Bound upper = null;
+ if (expression.upper != null)
+ {
+ final byte[] upperBound = toComparableBytes(bytesPerValue, expression.upper.value.encoded, expression.validator);
+ upper = new Bound(upperBound, !expression.upper.inclusive);
+ }
+
+ return new RangeQueryVisitor(bytesPerValue, lower, upper);
+ }
+
+ private static byte[] toComparableBytes(int bytesPerDim, ByteBuffer value, AbstractType<?> type)
+ {
+ byte[] buffer = new byte[TypeUtil.fixedSizeOf(type)];
+ assert buffer.length == bytesPerDim;
+ TypeUtil.toComparableBytes(value, type, buffer);
+ return buffer;
+ }
+
+ private static abstract class RangeQuery implements BlockBalancedTreeReader.IntersectVisitor
+ {
+ final int bytesPerValue;
+
+ RangeQuery(int bytesPerValue)
+ {
+ this.bytesPerValue = bytesPerValue;
+ }
+
+ int compareUnsigned(byte[] packedValue, Bound bound)
+ {
+ return FutureArrays.compareUnsigned(packedValue, 0, bytesPerValue, bound.bound, 0, bytesPerValue);
+ }
+ }
+
+ private static class Bound
+ {
+ private final byte[] bound;
+ private final boolean exclusive;
+
+ Bound(byte[] bound, boolean exclusive)
+ {
+ this.bound = bound;
+ this.exclusive = exclusive;
+ }
+
+ boolean smallerThan(int cmp)
+ {
+ return cmp > 0 || (cmp == 0 && exclusive);
+ }
+
+ boolean greaterThan(int cmp)
+ {
+ return cmp < 0 || (cmp == 0 && exclusive);
+ }
+ }
+
+ private static class RangeQueryVisitor extends RangeQuery
+ {
+ private final Bound lower;
+ private final Bound upper;
+
+ private RangeQueryVisitor(int bytesPerValue, Bound lower, Bound upper)
+ {
+ super(bytesPerValue);
+ this.lower = lower;
+ this.upper = upper;
+ }
+
+ @Override
+ public boolean visit(byte[] packedValue)
Review Comment:
I've chosen `contains` because this is pretty much what it does.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1245800030
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final TraversalState state;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInput indexInput = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(indexInput);
+ indexInput.seek(treeIndexRoot);
+
+ state = new TraversalState(indexInput);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return state.memoryUsage;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ state.reset();
+ traverse(callback, new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, IntArrayList pathToRoot)
+ {
+ if (state.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (state.nodeExists())
+ {
+ callback.onLeaf(state.nodeID, state.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(state.nodeID);
+
+ state.pushLeft();
+ traverse(callback, currentPath);
+ state.pop();
+
+ state.pushRight();
+ traverse(callback, currentPath);
+ state.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ /**
+ * This maintains the state for a traversal of the packed index. It is loaded once and can be resused
+ * by calling the reset method.
+ * <p>
+ * The packed index is a packed representation of a balanced tree and takes the form of a packed array of
+ * file pointer / split value pairs. Both the file pointers and split values are prefix compressed by tree level
+ * requiring us to maintain a stack of values for each level in the tree. The stack size is always the tree depth.
+ * <p>
+ * The tree is traversed by recursively following the left and then right subtrees under the current node. For the
+ * following tree (split values in square brackets):
+ * <pre>
+ * 1[16]
+ * / \
+ * / \
+ * 2[8] 3[24]
+ * / \ / \
+ * 4 5 6 7
+ * </pre>
+ * The traversal will be 1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 with nodes 4, 5, 6 & 7 being leaf nodes.
+ * <p>
+ * Assuming the full range of values in the tree is 0 -> 32, the non-leaf nodes will represent the following
+ * values:
+ * <pre>
+ * 1[0-32]
+ * / \
+ * 2[0-16] 3[16-32]
Review Comment:
Noticed the 16 is inclusive on both ends, but that's possible if we have too many identical values that overflow along leaf boundaries?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1246435889
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+ private final long maxRows;
+
+ public BlockBalancedTreeWriter(long maxRows, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxRows = maxRows;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxRows), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> leafWriter.add(packedValue, rowID));
+
+ valueCount = leafWriter.finish();
+
+ long treeFilePointer = valueCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ // There is only any point in writing the balanced tree if any values were added
+ if (treeFilePointer >= 0)
+ writeBalancedTree(treeOutput, maxPointsInLeafNode, leafWriter.leafBlockStartValues, leafWriter.leafBlockFilePointers);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return treeFilePointer;
+ }
+
+ private void writeBalancedTree(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitValues = new byte[(1 + numInnerNodes) * bytesPerValue];
+ int depth = recurseBalanceTree(1, 0, numInnerNodes, 1, splitValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ out.writeVInt(leafBlockFPs.length);
+ out.writeVInt(Math.min(depth, leafBlockFPs.length));
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(valueCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ /**
+ * This can, potentially, be removed in the future by CASSANDRA-18597
+ */
+ private int recurseBalanceTree(int nodeID, int offset, int count, int treeDepth, byte[] splitValues, List<byte[]> leafBlockStartValues)
+ {
+ treeDepth++;
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ int leftTreeDepth = recurseBalanceTree(2 * nodeID, offset, leftHalf, treeDepth, splitValues, leafBlockStartValues);
+
+ // Recurse right
+ int rightTreeDepth = recurseBalanceTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, treeDepth, splitValues, leafBlockStartValues);
+ return Math.max(leftTreeDepth, rightTreeDepth);
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
Review Comment:
Answering this and below here.
I have refined this calculation to be more accurate and have added testing to test that the tree depth is correct. I have moved the increment to only increment for tree nodes and leaf nodes. The tree depth starts at one to account for the root node.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1246441424
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+@NotThreadSafe
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ public static final boolean DEBUG = CassandraRelevantProperties.SAI_TEST_BALANCED_TREE_DEBUG_ENABLED.getBoolean();
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long valueCount;
+ private final long maxRows;
+
+ public BlockBalancedTreeWriter(long maxRows, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxRows = maxRows;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getValueCount()
+ {
+ return valueCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long write(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxRows), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ LeafWriter leafWriter = new LeafWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> leafWriter.add(packedValue, rowID));
+
+ valueCount = leafWriter.finish();
+
+ long treeFilePointer = valueCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ // There is only any point in writing the balanced tree if any values were added
+ if (treeFilePointer >= 0)
+ writeBalancedTree(treeOutput, maxPointsInLeafNode, leafWriter.leafBlockStartValues, leafWriter.leafBlockFilePointers);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return treeFilePointer;
+ }
+
+ private void writeBalancedTree(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitValues = new byte[(1 + numInnerNodes) * bytesPerValue];
+ int depth = recurseBalanceTree(1, 0, numInnerNodes, 1, splitValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ out.writeVInt(leafBlockFPs.length);
+ out.writeVInt(Math.min(depth, leafBlockFPs.length));
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(valueCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ /**
+ * This can, potentially, be removed in the future by CASSANDRA-18597
+ */
+ private int recurseBalanceTree(int nodeID, int offset, int count, int treeDepth, byte[] splitValues, List<byte[]> leafBlockStartValues)
+ {
+ treeDepth++;
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, splitValues, nodeID * bytesPerValue, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ int leftTreeDepth = recurseBalanceTree(2 * nodeID, offset, leftHalf, treeDepth, splitValues, leafBlockStartValues);
+
+ // Recurse right
+ int rightTreeDepth = recurseBalanceTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, treeDepth, splitValues, leafBlockStartValues);
+ return Math.max(leftTreeDepth, rightTreeDepth);
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ return treeDepth;
+ }
+
+ // Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure.
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValue = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitValues, 0, blocks, 1, lastSplitValue, false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValue is the split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValue, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ throw new IllegalStateException();
+// return 0;
Review Comment:
Note: In all the testing I have done on this, I haven't managed to create an unbalanced tree that gets here. As a result, I have added the `IllegalStateException` (with a message now). I will continue to investigate this to see if it's possible and we will get some performance testing done of this to see if we get here on larger, real world, datasets.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1246442243
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final TraversalState state;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInput indexInput = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(indexInput);
+ indexInput.seek(treeIndexRoot);
+
+ state = new TraversalState(indexInput);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return state.memoryUsage;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ state.reset();
+ traverse(callback, new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, IntArrayList pathToRoot)
+ {
+ if (state.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (state.nodeExists())
+ {
+ callback.onLeaf(state.nodeID, state.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(state.nodeID);
+
+ state.pushLeft();
+ traverse(callback, currentPath);
+ state.pop();
+
+ state.pushRight();
+ traverse(callback, currentPath);
+ state.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ /**
+ * This maintains the state for a traversal of the packed index. It is loaded once and can be resused
+ * by calling the reset method.
+ * <p>
+ * The packed index is a packed representation of a balanced tree and takes the form of a packed array of
+ * file pointer / split value pairs. Both the file pointers and split values are prefix compressed by tree level
+ * requiring us to maintain a stack of values for each level in the tree. The stack size is always the tree depth.
+ * <p>
+ * The tree is traversed by recursively following the left and then right subtrees under the current node. For the
+ * following tree (split values in square brackets):
+ * <pre>
+ * 1[16]
+ * / \
+ * / \
+ * 2[8] 3[24]
+ * / \ / \
+ * 4 5 6 7
+ * </pre>
+ * The traversal will be 1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 with nodes 4, 5, 6 & 7 being leaf nodes.
+ * <p>
+ * Assuming the full range of values in the tree is 0 -> 32, the non-leaf nodes will represent the following
+ * values:
+ * <pre>
+ * 1[0-32]
+ * / \
+ * 2[0-16] 3[16-32]
+ * </pre>
+ */
+ final static class TraversalState
+ {
+ final int bytesPerValue;
+ final int numLeaves;
+ final int treeDepth;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+ final long memoryUsage;
+
+ // used to read the packed index byte[]
+ final ByteArrayDataInput dataInput;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ final int[] rightNodePositions;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ final byte[][] splitValuesStack;
+
+ int nodeID;
+ int level;
+
+ TraversalState(DataInput dataInput) throws IOException
+ {
+ maxPointsInLeafNode = dataInput.readVInt();
+ bytesPerValue = dataInput.readVInt();
+
+ // Read index:
+ numLeaves = dataInput.readVInt();
+ assert numLeaves > 0;
+ treeDepth = dataInput.readVInt();
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+
+ dataInput.readBytes(minPackedValue, 0, bytesPerValue);
+ dataInput.readBytes(maxPackedValue, 0, bytesPerValue);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, dataInput);
+ }
+
+ pointCount = dataInput.readVLong();
+
+ int numBytes = dataInput.readVInt();
+ byte[] packedIndex = new byte[numBytes];
+ dataInput.readBytes(packedIndex, 0, numBytes);
+
+ memoryUsage = ObjectSizes.sizeOfArray(packedIndex) + ObjectSizes.sizeOfArray(minPackedValue) + ObjectSizes.sizeOfArray(maxPackedValue);
+
+ nodeID = 1;
+ level = 0;
+ leafBlockFPStack = new long[treeDepth];
+ leftNodePositions = new int[treeDepth];
+ rightNodePositions = new int[treeDepth];
+ splitValuesStack = new byte[treeDepth][];
+ this.dataInput = new ByteArrayDataInput(packedIndex);
+ readNodeData(false);
+ }
+
+ public void reset()
+ {
+ nodeID = 1;
+ level = 0;
+ dataInput.setPosition(0);
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ dataInput.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ dataInput.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ }
+
+ public boolean isLeafNode()
Review Comment:
I agree, `atLeafNode` reads better because this is a traversal state so is telling us where we are.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1246928540
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ByteArrayUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final TraversalState state;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInput indexInput = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(indexInput);
+ indexInput.seek(treeIndexRoot);
+
+ state = new TraversalState(indexInput);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return state.memoryUsage;
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ state.reset();
+ traverse(callback, new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, IntArrayList pathToRoot)
+ {
+ if (state.atLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (state.nodeExists())
+ {
+ callback.onLeaf(state.nodeID, state.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(state.nodeID);
+
+ state.pushLeft();
+ traverse(callback, currentPath);
+ state.pop();
+
+ state.pushRight();
+ traverse(callback, currentPath);
+ state.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ /**
+ * This maintains the state for a traversal of the packed index. It is loaded once and can be resused
+ * by calling the reset method.
+ * <p>
+ * The packed index is a packed representation of a balanced tree and takes the form of a packed array of
+ * file pointer / split value pairs. Both the file pointers and split values are prefix compressed by tree level
+ * requiring us to maintain a stack of values for each level in the tree. The stack size is always the tree depth.
+ * <p>
+ * The tree is traversed by recursively following the left and then right subtrees under the current node. For the
+ * following tree (split values in square brackets):
+ * <pre>
+ * 1[16]
+ * / \
+ * / \
+ * 2[8] 3[24]
+ * / \ / \
+ * 4 5 6 7
+ * </pre>
+ * The traversal will be 1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 with nodes 4, 5, 6 & 7 being leaf nodes.
+ * <p>
+ * Assuming the full range of values in the tree is 0 -> 32, the non-leaf nodes will represent the following
+ * values:
+ * <pre>
+ * 1[0-32]
+ * / \
+ * 2[0-16] 3[16-32]
+ * </pre>
+ */
+ final static class TraversalState
+ {
+ final int bytesPerValue;
+ final int numLeaves;
+ final int treeDepth;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ final long valueCount;
+ final int maxPointsInLeafNode;
+ final long memoryUsage;
+
+ // used to read the packed index byte[]
+ final ByteArrayDataInput dataInput;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ final int[] rightNodePositions;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ final byte[][] splitValuesStack;
+
+ int nodeID;
+ int level;
+ @VisibleForTesting
+ int maxLevel;
+
+ TraversalState(DataInput dataInput) throws IOException
+ {
+ maxPointsInLeafNode = dataInput.readVInt();
+ bytesPerValue = dataInput.readVInt();
+
+ // Read index:
+ numLeaves = dataInput.readVInt();
+ assert numLeaves > 0;
+ treeDepth = dataInput.readVInt();
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+
+ dataInput.readBytes(minPackedValue, 0, bytesPerValue);
+ dataInput.readBytes(maxPackedValue, 0, bytesPerValue);
+
+ if (ByteArrayUtil.compareUnsigned(minPackedValue, 0, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, dataInput);
+ }
+
+ valueCount = dataInput.readVLong();
+
+ int numBytes = dataInput.readVInt();
+ byte[] packedIndex = new byte[numBytes];
+ dataInput.readBytes(packedIndex, 0, numBytes);
+
+ nodeID = 1;
+ level = 0;
+ leafBlockFPStack = new long[treeDepth];
+ leftNodePositions = new int[treeDepth];
+ rightNodePositions = new int[treeDepth];
+ splitValuesStack = new byte[treeDepth][];
+
+ memoryUsage = ObjectSizes.sizeOfArray(packedIndex) +
+ ObjectSizes.sizeOfArray(minPackedValue) +
+ ObjectSizes.sizeOfArray(maxPackedValue) +
+ ObjectSizes.sizeOfArray(leafBlockFPStack) +
+ ObjectSizes.sizeOfArray(leftNodePositions) +
+ ObjectSizes.sizeOfArray(rightNodePositions) +
+ ObjectSizes.sizeOfArray(splitValuesStack) * bytesPerValue;
+
+ this.dataInput = new ByteArrayDataInput(packedIndex);
+ readNodeData(false);
+ }
+
+ public void reset()
+ {
+ nodeID = 1;
+ level = 0;
+ dataInput.setPosition(0);
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ maxLevel = Math.max(maxLevel, level);
+ dataInput.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ maxLevel = Math.max(maxLevel, level);
+ dataInput.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ }
+
+ public boolean atLeafNode()
+ {
+ return nodeID >= numLeaves;
+ }
+
+ public boolean nodeExists()
+ {
+ return nodeID - numLeaves < numLeaves;
+ }
+
+ public long getLeafBlockFP()
+ {
+ return leafBlockFPStack[level];
+ }
+
+ public byte[] getSplitValue()
+ {
+ assert !atLeafNode();
Review Comment:
```suggestion
assert !atLeafNode() : "Leaf nodes do not have split values";
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1247226182
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeRamBuffer.java:
##########
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.apache.cassandra.db.memtable.TrieMemtable;
+import org.apache.cassandra.db.tries.InMemoryTrie;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.cassandra.utils.bytecomparable.ByteSource;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+/**
+ * On-heap buffer for values that provides a sorted view of itself as a {@link BlockBalancedTreeIterator}.
+ */
+@NotThreadSafe
+public class BlockBalancedTreeRamBuffer
+{
+ private final InMemoryTrie<PackedLongValues.Builder> trieBuffer;
Review Comment:
nit: Could even shorten it to just `trie` instead of `trieBuffer` and I don't think it would sacrifice any clarity.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1244055126
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWalker.java:
##########
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class BlockBalancedTreeWalker implements Closeable
+{
+ final FileHandle treeIndexFile;
+ final int bytesPerValue;
+ final int numLeaves;
+ final byte[] minPackedValue;
+ final byte[] maxPackedValue;
+ // Packed array of byte[] holding all split values in the full binary tree:
+ final byte[] packedIndex;
+ final long pointCount;
+ final int maxPointsInLeafNode;
+
+ BlockBalancedTreeWalker(FileHandle treeIndexFile, long treeIndexRoot)
+ {
+ this.treeIndexFile = treeIndexFile;
+
+ try (RandomAccessReader reader = treeIndexFile.createReader();
+ IndexInputReader in = IndexInputReader.create(reader))
+ {
+ SAICodecUtils.validate(in);
+ in.seek(treeIndexRoot);
+
+ maxPointsInLeafNode = in.readVInt();
+ bytesPerValue = in.readVInt();
+
+ // Read index:
+ numLeaves = in.readVInt();
+ assert numLeaves > 0;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+
+ in.readBytes(minPackedValue, 0, bytesPerValue);
+ in.readBytes(maxPackedValue, 0, bytesPerValue);
+
+ if (FutureArrays.compareUnsigned(minPackedValue, 0, bytesPerValue, maxPackedValue, 0, bytesPerValue) > 0)
+ {
+ String message = String.format("Min packed value %s is > max packed value %s.",
+ new BytesRef(minPackedValue), new BytesRef(maxPackedValue));
+ throw new CorruptIndexException(message, in);
+ }
+
+ pointCount = in.readVLong();
+
+ int numBytes = in.readVInt();
+ packedIndex = new byte[numBytes];
+ in.readBytes(packedIndex, 0, numBytes);
+ }
+ catch (Throwable t)
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ throw Throwables.unchecked(t);
+ }
+ }
+
+ public long memoryUsage()
+ {
+ return ObjectSizes.sizeOfArray(packedIndex)
+ + ObjectSizes.sizeOfArray(minPackedValue)
+ + ObjectSizes.sizeOfArray(maxPackedValue);
+ }
+
+ @Override
+ public void close()
+ {
+ FileUtils.closeQuietly(treeIndexFile);
+ }
+
+ void traverse(TraversalCallback callback)
+ {
+ traverse(callback, new PackedIndexTree(packedIndex, bytesPerValue, numLeaves), new IntArrayList());
+ }
+
+ private void traverse(TraversalCallback callback, PackedIndexTree index, IntArrayList pathToRoot)
+ {
+ if (index.isLeafNode())
+ {
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (index.nodeExists())
+ {
+ callback.onLeaf(index.getNodeID(), index.getLeafBlockFP(), pathToRoot);
+ }
+ }
+ else
+ {
+ int nodeID = index.getNodeID();
+ IntArrayList currentPath = new IntArrayList();
+ currentPath.addAll(pathToRoot);
+ currentPath.add(nodeID);
+
+ index.pushLeft();
+ traverse(callback, index, currentPath);
+ index.pop();
+
+ index.pushRight();
+ traverse(callback, index, currentPath);
+ index.pop();
+ }
+ }
+
+ interface TraversalCallback
+ {
+ void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot);
+ }
+
+ final static class PackedIndexTree
+ {
+ private final int bytesPerValue;
+ private final int numLeaves;
+
+ private final byte[][] splitPackedValueStack;
+ // used to read the packed index byte[]
+ private final ByteArrayDataInput in;
+ // holds the minimum (left most) leaf block file pointer for each level we've recursed to:
+ private final long[] leafBlockFPStack;
+ // holds the address, in the packed byte[] index, of the left-node of each level:
+ private final int[] leftNodePositions;
+ // holds the address, in the packed byte[] index, of the right-node of each level:
+ private final int[] rightNodePositions;
+ // true if the delta we read for the node at this level is a negative offset vs. the last split;
+ // this will be true if the last time we split, we next pushed to the left subtree:
+ private final boolean[] negativeDeltas;
+ // holds the packed per-level split values; the run method uses this to save the cell min/max as it recurses:
+ private final byte[][] splitValuesStack;
+
+ private int nodeID;
+ // level is 1-based so that we can do level-1 w/o checking each time:
+ private int level;
+ private boolean leafNode;
+
+ PackedIndexTree(byte[] packedIndex, int bytesPerValue, int numLeaves)
+ {
+ this.bytesPerValue = bytesPerValue;
+ this.numLeaves = numLeaves;
+ int treeDepth = getTreeDepth();
+ splitPackedValueStack = new byte[treeDepth + 1][];
+ nodeID = 1;
+ level = 1;
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ leafBlockFPStack = new long[treeDepth + 1];
+ leftNodePositions = new int[treeDepth + 1];
+ rightNodePositions = new int[treeDepth + 1];
+ splitValuesStack = new byte[treeDepth + 1][];
+ negativeDeltas = new boolean[treeDepth + 1];
+
+ in = new ByteArrayDataInput(packedIndex);
+ splitValuesStack[0] = new byte[bytesPerValue];
+ readNodeData(false);
+ }
+
+ public void pushLeft()
+ {
+ int nodePosition = leftNodePositions[level];
+ nodeID *= 2;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = true;
+ in.setPosition(nodePosition);
+ readNodeData(true);
+ }
+
+ public void pushRight()
+ {
+ int nodePosition = rightNodePositions[level];
+ nodeID = nodeID * 2 + 1;
+ level++;
+ if (splitPackedValueStack[level] == null)
+ splitPackedValueStack[level] = new byte[bytesPerValue];
+ System.arraycopy(negativeDeltas, level - 1, negativeDeltas, level, 1);
+ assert !leafNode;
+ negativeDeltas[level] = false;
+ in.setPosition(nodePosition);
+ readNodeData(false);
+ }
+
+ public void pop()
+ {
+ nodeID /= 2;
+ level--;
+ leafNode = false;
+ }
+
+ public boolean isLeafNode()
+ {
+ return nodeID >= numLeaves;
+ }
+
+ public boolean nodeExists()
+ {
+ return nodeID - numLeaves < numLeaves;
+ }
+
+ public int getNodeID()
+ {
+ return nodeID;
+ }
+
+ public byte[] getSplitPackedValue()
+ {
+ assert !isLeafNode();
+ assert splitPackedValueStack[level] != null : "level=" + level;
+ return splitPackedValueStack[level];
+ }
+
+ public long getLeafBlockFP()
+ {
+ assert isLeafNode() : "nodeID=" + nodeID + " is not a leaf";
+ return leafBlockFPStack[level];
+ }
+
+ public byte[] getSplitValue()
+ {
+ assert !isLeafNode();
+ return splitValuesStack[level];
+ }
+
+ private void readNodeData(boolean isLeft)
+ {
+ leafBlockFPStack[level] = leafBlockFPStack[level - 1];
+
+ // read leaf block FP delta
+ if (!isLeft)
+ leafBlockFPStack[level] += in.readVLong();
+
+ leafNode = isLeafNode();
+ if (!leafNode)
+ {
+ // read prefix, firstDiffByteDelta encoded as int:
+ int code = in.readVInt();
+ int prefix = code % (1 + bytesPerValue);
+ int suffix = bytesPerValue - prefix;
+
+ if (splitValuesStack[level] == null)
+ {
+ splitValuesStack[level] = new byte[bytesPerValue];
+ }
+ System.arraycopy(splitValuesStack[level - 1], 0, splitValuesStack[level], 0, bytesPerValue);
+ if (suffix > 0)
+ {
+ int firstDiffByteDelta = code / (1 + bytesPerValue);
+ if (negativeDeltas[level])
+ firstDiffByteDelta = -firstDiffByteDelta;
+ int oldByte = splitValuesStack[level][prefix] & 0xFF;
+ splitValuesStack[level][prefix] = (byte) (oldByte + firstDiffByteDelta);
+ in.readBytes(splitValuesStack[level], prefix + 1, suffix - 1);
+ }
+
+ int leftNumBytes = nodeID * 2 < numLeaves ? in.readVInt() : 0;
+
+ leftNodePositions[level] = in.getPosition();
+ rightNodePositions[level] = leftNodePositions[level] + leftNumBytes;
+ }
+ }
+
+ private int getTreeDepth()
+ {
+ // First +1 because all the non-leave nodes makes another power
+ // of 2; e.g. to have a fully balanced tree with 4 leaves you
+ // need a depth=3 tree:
+
+ // Second +1 because MathUtil.log computes floor of the logarithm; e.g.
+ // with 5 leaves you need a depth=4 tree:
Review Comment:
> I am thinking that we could just add the tree depth to the on-disk format because the write side must know the value.
That doesn't sound like a bad idea.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239711573
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(treeOutput.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
Review Comment:
I have successfully (I think) removed all dimensionality from here.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1239683239
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
Review Comment:
I've added a brief interim javadoc but would prefer we get rid of this entirely.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1242139985
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.BKDWriter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the Lucene {@link BKDWriter} that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ * <p>
+ * @see BKDWriter
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = true;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ * <p>
+ * Visual representation of the disk format:
+ * <pre>
+ *
+ * +========+=======================================+==================+========+
+ * | HEADER | LEAF BLOCK LIST | BALANCED TREE | FOOTER |
+ * +========+================+=====+================+==================+========+
+ * | LEAF BLOCK (0) | ... | LEAF BLOCK (N) | VALUES PER LEAF |
+ * +----------------+-----+----------------+------------------|
+ * | ORDER INDEX | | BYTES PER VALUE |
+ * +----------------+ +------------------+
+ * | PREFIX | | NUMBER OF LEAVES |
+ * +----------------+ +------------------+
+ * | VALUES | | MINIMUM VALUE |
+ * +----------------+ +------------------+
+ * | MAXIMUM VALUE |
+ * +------------------+
+ * | TOTAL VALUES |
+ * +------------------+
+ * | INDEX TREE |
+ * +--------+---------+
+ * | LENGTH | BYTES |
+ * +--------+---------+
+ * </pre>
+ *
+ * @param treeOutput The {@link IndexOutput} to write the balanced tree to
+ * @param reader The {@link IntersectingPointValues} containing the values and rowIDs to be written
+ * @param callback The {@link Callback} used to record the leaf postings for each leaf
+ *
+ * @return The file pointer to the beginning of the balanced tree
+ */
+ public long writeTree(IndexOutput treeOutput, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(treeOutput);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ TreeWriter treeWriter = new TreeWriter(treeOutput, callback);
+
+ reader.intersect((rowID, packedValue) -> treeWriter.add(packedValue, rowID));
+
+ pointCount = treeWriter.finish();
+
+ long filePointer = pointCount == 0 ? -1 : treeOutput.getFilePointer();
+
+ writeIndex(treeOutput, maxPointsInLeafNode, treeWriter.leafBlockStartValues, treeWriter.leafBlockFilePointer);
+
+ SAICodecUtils.writeFooter(treeOutput);
+
+ return filePointer;
+ }
+
+ private void writeIndex(IndexOutput out, int countPerLeaf, List<byte[]> leafBlockStartValues, List<Long> leafBlockFilePointer) throws IOException
+ {
+ int numInnerNodes = leafBlockStartValues.size();
+ byte[] splitPackedValues = new byte[(1 + numInnerNodes) * (1 + bytesPerValue)];
+ rotateToTree(1, 0, numInnerNodes, splitPackedValues, leafBlockStartValues);
+ long[] leafBlockFPs = leafBlockFilePointer.stream().mapToLong(l -> l).toArray();
+ byte[] packedIndex = packIndex(leafBlockFPs, splitPackedValues);
+
+ out.writeVInt(countPerLeaf);
+ out.writeVInt(bytesPerValue);
+
+ assert leafBlockFPs.length > 0;
+ out.writeVInt(leafBlockFPs.length);
+
+ out.writeBytes(minPackedValue, 0, bytesPerValue);
+ out.writeBytes(maxPackedValue, 0, bytesPerValue);
+
+ out.writeVLong(pointCount);
+
+ out.writeVInt(packedIndex.length);
+ out.writeBytes(packedIndex, 0, packedIndex.length);
+ }
+
+ private void rotateToTree(int nodeID, int offset, int count, byte[] index, List<byte[]> leafBlockStartValues)
+ {
+ if (count == 1)
+ {
+ // Leaf index node
+ System.arraycopy(leafBlockStartValues.get(offset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+ }
+ else if (count > 1)
+ {
+ // Internal index node: binary partition of count
+ int countAtLevel = 1;
+ int totalCount = 0;
+ while (true)
+ {
+ int countLeft = count - totalCount;
+ if (countLeft <= countAtLevel)
+ {
+ // This is the last level, possibly partially filled:
+ int lastLeftCount = Math.min(countAtLevel / 2, countLeft);
+ assert lastLeftCount >= 0;
+ int leftHalf = (totalCount - 1) / 2 + lastLeftCount;
+
+ int rootOffset = offset + leftHalf;
+
+ System.arraycopy(leafBlockStartValues.get(rootOffset), 0, index, nodeID * (1 + bytesPerValue) + 1, bytesPerValue);
+
+ // TODO: we could optimize/specialize, when we know it's simply fully balanced binary tree
+ // under here, to save this while loop on each recursion
+
+ // Recurse left
+ rotateToTree(2 * nodeID, offset, leftHalf, index, leafBlockStartValues);
+
+ // Recurse right
+ rotateToTree(2 * nodeID + 1, rootOffset + 1, count - leftHalf - 1, index, leafBlockStartValues);
+ return;
+ }
+ totalCount += countAtLevel;
+ countAtLevel *= 2;
+ }
+ }
+ else
+ {
+ assert count == 0;
+ }
+ }
+
+ /** Packs the two arrays, representing a balanced binary tree, into a compact byte[] structure. */
+ private byte[] packIndex(long[] leafBlockFPs, byte[] splitPackedValues) throws IOException
+ {
+ int numLeaves = leafBlockFPs.length;
+
+ // Possibly rotate the leaf block FPs, if the index is not a fully balanced binary tree (only happens
+ // if it was created by TreeWriter). In this case the leaf nodes may straddle the two bottom
+ // levels of the binary tree:
+ if (numLeaves > 1)
+ {
+ int levelCount = 2;
+ while (true)
+ {
+ if (numLeaves >= levelCount && numLeaves <= 2 * levelCount)
+ {
+ int lastLevel = 2 * (numLeaves - levelCount);
+ assert lastLevel >= 0;
+ if (lastLevel != 0)
+ {
+ // Last level is partially filled, so we must rotate the leaf FPs to match. We do this here, after loading
+ // at read-time, so that we can still delta code them on disk at write:
+ long[] newLeafBlockFPs = new long[numLeaves];
+ System.arraycopy(leafBlockFPs, lastLevel, newLeafBlockFPs, 0, leafBlockFPs.length - lastLevel);
+ System.arraycopy(leafBlockFPs, 0, newLeafBlockFPs, leafBlockFPs.length - lastLevel, lastLevel);
+ leafBlockFPs = newLeafBlockFPs;
+ }
+ break;
+ }
+
+ levelCount *= 2;
+ }
+ }
+
+ // Reused while packing the index
+ try (RAMIndexOutput writeBuffer = new RAMIndexOutput("PackedIndex"))
+ {
+ // This is the "file" we append the byte[] to:
+ List<byte[]> blocks = new ArrayList<>();
+ byte[] lastSplitValues = new byte[bytesPerValue];
+ int totalSize = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, 0, blocks, 1, lastSplitValues, new boolean[1], false);
+ // Compact the byte[] blocks into single byte index:
+ byte[] index = new byte[totalSize];
+ int upto = 0;
+ for (byte[] block : blocks)
+ {
+ System.arraycopy(block, 0, index, upto, block.length);
+ upto += block.length;
+ }
+ assert upto == totalSize;
+
+ return index;
+ }
+ }
+
+ /**
+ * lastSplitValues is per-dimension split value previously seen; we use this to prefix-code the split byte[] on each
+ * inner node
+ */
+ private int recursePackIndex(RAMIndexOutput writeBuffer, long[] leafBlockFPs, byte[] splitPackedValues, long minBlockFP, List<byte[]> blocks,
+ int nodeID, byte[] lastSplitValues, boolean[] negativeDeltas, boolean isLeft) throws IOException
+ {
+ if (nodeID >= leafBlockFPs.length)
+ {
+ int leafID = nodeID - leafBlockFPs.length;
+
+ // In the unbalanced case it's possible the left most node only has one child:
+ if (leafID < leafBlockFPs.length)
+ {
+ long delta = leafBlockFPs[leafID] - minBlockFP;
+ if (isLeft)
+ {
+ assert delta == 0;
+ return 0;
+ }
+ else
+ {
+ assert nodeID == 1 || delta > 0 : "nodeID=" + nodeID;
+ writeBuffer.writeVLong(delta);
+ return appendBlock(writeBuffer, blocks);
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ long leftBlockFP;
+ if (!isLeft)
+ {
+ leftBlockFP = getLeftMostLeafBlockFP(leafBlockFPs, nodeID);
+ long delta = leftBlockFP - minBlockFP;
+ assert nodeID == 1 || delta > 0;
+ writeBuffer.writeVLong(delta);
+ }
+ else
+ {
+ // The left tree's left most leaf block FP is always the minimal FP:
+ leftBlockFP = minBlockFP;
+ }
+
+ int address = nodeID * (1 + bytesPerValue);
+ int splitDim = splitPackedValues[address++] & 0xff;
+
+ // find common prefix with last split value in this dim:
+ int prefix = 0;
+ for (; prefix < bytesPerValue; prefix++)
+ {
+ if (splitPackedValues[address + prefix] != lastSplitValues[splitDim * bytesPerValue + prefix])
+ {
+ break;
+ }
+ }
+
+ int firstDiffByteDelta;
+ if (prefix < bytesPerValue)
+ {
+ firstDiffByteDelta = (splitPackedValues[address + prefix] & 0xFF) - (lastSplitValues[splitDim * bytesPerValue + prefix] & 0xFF);
+ if (negativeDeltas[splitDim])
+ {
+ firstDiffByteDelta = -firstDiffByteDelta;
+ }
+ assert firstDiffByteDelta > 0;
+ }
+ else
+ {
+ firstDiffByteDelta = 0;
+ }
+
+ // pack the prefix, splitDim and delta first diff byte into a single vInt:
+ int code = (firstDiffByteDelta * (1 + bytesPerValue) + prefix) + splitDim;
+
+ writeBuffer.writeVInt(code);
+
+ // write the split value, prefix coded vs. our parent's split value:
+ int suffix = bytesPerValue - prefix;
+ byte[] savSplitValue = new byte[suffix];
+ if (suffix > 1)
+ {
+ writeBuffer.writeBytes(splitPackedValues, address + prefix + 1, suffix - 1);
+ }
+
+ byte[] cmp = lastSplitValues.clone();
+
+ System.arraycopy(lastSplitValues, splitDim * bytesPerValue + prefix, savSplitValue, 0, suffix);
+
+ // copy our split value into lastSplitValues for our children to prefix-code against
+ System.arraycopy(splitPackedValues, address + prefix, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ int numBytes = appendBlock(writeBuffer, blocks);
+
+ // placeholder for left-tree numBytes; we need this so that at search time if we only need to recurse into
+ // the right subtree we can quickly seek to its starting point
+ int idxSav = blocks.size();
+ blocks.add(null);
+
+ boolean savNegativeDelta = negativeDeltas[splitDim];
+ negativeDeltas[splitDim] = true;
+
+ int leftNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID, lastSplitValues, negativeDeltas, true);
+
+ if (nodeID * 2 < leafBlockFPs.length)
+ {
+ writeBuffer.writeVInt(leftNumBytes);
+ }
+ else
+ {
+ assert leftNumBytes == 0 : "leftNumBytes=" + leftNumBytes;
+ }
+ int numBytes2 = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes2 = new byte[numBytes2];
+ writeBuffer.writeTo(bytes2);
+ writeBuffer.reset();
+ // replace our placeholder:
+ blocks.set(idxSav, bytes2);
+
+ negativeDeltas[splitDim] = false;
+ int rightNumBytes = recursePackIndex(writeBuffer, leafBlockFPs, splitPackedValues, leftBlockFP, blocks, 2 * nodeID + 1, lastSplitValues, negativeDeltas, false);
+
+ negativeDeltas[splitDim] = savNegativeDelta;
+
+ // restore lastSplitValues to what caller originally passed us:
+ System.arraycopy(savSplitValue, 0, lastSplitValues, splitDim * bytesPerValue + prefix, suffix);
+
+ assert Arrays.equals(lastSplitValues, cmp);
+
+ return numBytes + numBytes2 + leftNumBytes + rightNumBytes;
+ }
+ }
+
+ /** Appends the current contents of writeBuffer as another block on the growing in-memory file */
+ private int appendBlock(RAMIndexOutput writeBuffer, List<byte[]> blocks)
+ {
+ int pos = Math.toIntExact(writeBuffer.getFilePointer());
+ byte[] bytes = new byte[pos];
+ writeBuffer.writeTo(bytes);
+ writeBuffer.reset();
+ blocks.add(bytes);
+ return pos;
+ }
+
+ private long getLeftMostLeafBlockFP(long[] leafBlockFPs, int nodeID)
+ {
+ // TODO: can we do this cheaper, e.g. a closed form solution instead of while loop? Or
+ // change the recursion while packing the index to return this left-most leaf block FP
+ // from each recursion instead?
+ //
+ // Still, the overall cost here is minor: this method's cost is O(log(N)), and while writing
+ // we call it O(N) times (N = number of leaf blocks)
+ while (nodeID < leafBlockFPs.length)
+ {
+ nodeID *= 2;
+ }
+ int leafID = nodeID - leafBlockFPs.length;
+ long result = leafBlockFPs[leafID];
+ if (result < 0)
+ {
+ throw new AssertionError(result + " for leaf " + leafID);
+ }
+ return result;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class TreeWriter
+ {
+ private final IndexOutput treeOutput;
+ private final List<Long> leafBlockFilePointer = new ArrayList<>();
+ private final List<byte[]> leafBlockStartValues = new ArrayList<>();
+ private final byte[] leafValues = new byte[maxPointsInLeafNode * bytesPerValue];
+ private final long[] leafRowIDs = new long[maxPointsInLeafNode];
+ private final RowIDAndIndex[] rowIDAndIndexes = new RowIDAndIndex[maxPointsInLeafNode];
+ private final int[] orderIndex = new int[maxPointsInLeafNode];
+ private final Callback callback;
+ private final GrowableByteArrayDataOutput scratchOut = new GrowableByteArrayDataOutput(32 * 1024);
+ private final GrowableByteArrayDataOutput scratchOut2 = new GrowableByteArrayDataOutput(2 * 1024);
+ private final byte[] lastPackedValue = new byte[bytesPerValue];
+
+ private long valueCount;
+ private int leafValueCount;
+ private long lastRowID;
+
+ TreeWriter(IndexOutput treeOutput, Callback callback)
+ {
+ assert callback != null : "Callback cannot be null in TreeWriter";
+
+ this.treeOutput = treeOutput;
+ this.callback = callback;
+
+ for (int x = 0; x < rowIDAndIndexes.length; x++)
+ {
+ rowIDAndIndexes[x] = new RowIDAndIndex();
+ }
+ }
+
+ void add(byte[] packedValue, long docID) throws IOException
+ {
+ if (DEBUG)
+ valueInOrder(valueCount + leafValueCount, lastPackedValue, packedValue, 0, docID, lastRowID);
+
+ System.arraycopy(packedValue, 0, leafValues, leafValueCount * bytesPerValue, bytesPerValue);
+ leafRowIDs[leafValueCount] = docID;
+ leafValueCount++;
+
+ if (leafValueCount == maxPointsInLeafNode)
+ {
+ // We write a block once we hit exactly the max count
+ writeLeafBlock();
+ leafValueCount = 0;
+ }
+
+ if (DEBUG)
+ if ((lastRowID = docID) < 0)
+ throw new AssertionError("document id must be >= 0; got " + docID);
+ }
+
+ /**
+ * Write a leaf block if we have unwritten values and return the total number of values added
+ */
+ public long finish() throws IOException
+ {
+ if (leafValueCount > 0)
+ writeLeafBlock();
+
+ return valueCount;
+ }
+
+ private void writeLeafBlock() throws IOException
+ {
+ assert leafValueCount != 0;
+ if (valueCount == 0)
+ {
+ System.arraycopy(leafValues, 0, minPackedValue, 0, bytesPerValue);
+ }
+ System.arraycopy(leafValues, (leafValueCount - 1) * bytesPerValue, maxPackedValue, 0, bytesPerValue);
+
+ valueCount += leafValueCount;
+
+ if (leafBlockFilePointer.size() > 0)
+ {
+ // Save the first (minimum) value in each leaf block except the first, to build the split value index in the end:
+ leafBlockStartValues.add(ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue));
+ }
+ leafBlockFilePointer.add(treeOutput.getFilePointer());
+ checkMaxLeafNodeCount(leafBlockFilePointer.size());
+
+ // Find per-dim common prefix:
+ int commonPrefixLength = bytesPerValue;
+ int offset = (leafValueCount - 1) * bytesPerValue;
+ for (int j = 0; j < bytesPerValue; j++)
+ {
+ if (leafValues[j] != leafValues[offset + j])
+ {
+ commonPrefixLength = j;
+ break;
+ }
+ }
+
+ assert scratchOut.getPosition() == 0;
+
+ treeOutput.writeVInt(leafValueCount);
+
+ for (int x = 0; x < leafValueCount; x++)
+ {
+ rowIDAndIndexes[x].valueOrderIndex = x;
+ rowIDAndIndexes[x].rowID = leafRowIDs[x];
+ }
+
+ final Sorter sorter = new IntroSorter()
+ {
+ RowIDAndIndex pivot;
+
+ @Override
+ protected void swap(int i, int j)
+ {
+ RowIDAndIndex o = rowIDAndIndexes[i];
+ rowIDAndIndexes[i] = rowIDAndIndexes[j];
+ rowIDAndIndexes[j] = o;
+ }
+
+ @Override
+ protected void setPivot(int i)
+ {
+ pivot = rowIDAndIndexes[i];
+ }
+
+ @Override
+ protected int comparePivot(int j)
+ {
+ return Long.compare(pivot.rowID, rowIDAndIndexes[j].rowID);
+ }
+ };
+
+ sorter.sort(0, leafValueCount);
+
+ // write leaf rowID -> orig index
+ scratchOut2.reset();
+
+ // iterate in row ID order to get the row ID index for the given value order index
+ // place into an array to be written as packed ints
+ for (int x = 0; x < leafValueCount; x++)
+ orderIndex[rowIDAndIndexes[x].valueOrderIndex] = x;
+
+ LeafOrderMap.write(orderIndex, leafValueCount, maxPointsInLeafNode - 1, scratchOut2);
+
+ treeOutput.writeVInt(scratchOut2.getPosition());
+ treeOutput.writeBytes(scratchOut2.getBytes(), 0, scratchOut2.getPosition());
+
+ callback.writeLeafDocs(rowIDAndIndexes, 0, leafValueCount);
+
+ writeCommonPrefix(scratchOut, commonPrefixLength, leafValues);
+
+ scratchBytesRef1.length = bytesPerValue;
+ scratchBytesRef1.bytes = leafValues;
+
+ IntFunction<BytesRef> packedValues = (i) -> {
+ scratchBytesRef1.offset = bytesPerValue * i;
+ return scratchBytesRef1;
+ };
+ if (DEBUG)
+ valuesInOrderAndBounds(leafValueCount,
+ ArrayUtil.copyOfSubArray(leafValues, 0, bytesPerValue),
+ ArrayUtil.copyOfSubArray(leafValues, (leafValueCount - 1) * bytesPerValue, leafValueCount * bytesPerValue),
+ packedValues,
+ leafRowIDs);
+
+ writeLeafBlockPackedValues(scratchOut, commonPrefixLength, leafValueCount, packedValues);
+
+ treeOutput.writeBytes(scratchOut.getBytes(), 0, scratchOut.getPosition());
+
+ scratchOut.reset();
+ }
+
+ private void checkMaxLeafNodeCount(int numLeaves)
+ {
+ if ((1 + bytesPerValue) * (long) numLeaves > ArrayUtil.MAX_ARRAY_LENGTH)
+ {
+ throw new IllegalStateException("too many nodes; increase maxPointsInLeafNode (currently " + maxPointsInLeafNode + ") and reindex");
+ }
+ }
+
+ private void writeCommonPrefix(DataOutput treeOutput, int commonPrefixLength, byte[] packedValue) throws IOException
+ {
+ treeOutput.writeVInt(commonPrefixLength);
+ if (commonPrefixLength > 0)
+ treeOutput.writeBytes(packedValue, 0, commonPrefixLength);
+ }
+
+ private void writeLeafBlockPackedValues(DataOutput out, int commonPrefixLength, int count, IntFunction<BytesRef> packedValues) throws IOException
+ {
+ if (commonPrefixLength == bytesPerValue)
+ {
+ // all values in this block are equal
+ out.writeByte((byte) -1);
+ }
+ else
+ {
+ assert commonPrefixLength < bytesPerValue;
Review Comment:
I've removed it. The `commonPrefixLength` is calculated just before this method call and really can't fail this assertion. I find these assertions pointless and possibly dangerous because they cast doubt on things that really can't be wrong.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228159510
##########
src/java/org/apache/cassandra/index/sai/QueryContext.java:
##########
@@ -49,6 +49,12 @@
public long trieSegmentsHit = 0;
+ public long bkdPostingListsHit = 0;
+ public long bkdSegmentsHit = 0;
+
Review Comment:
I have changed these to use `bbt` as a prefix. I think we need to use this rather than use `BlockBalancedTree` everywhere.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228355204
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a BKD tree with auxiliary posting lists on eligible
+ * tree levels.
+ *
+ * Given sorted input {@link org.apache.lucene.codecs.MutablePointValues}, 1-dim case allows to optimise flush process, because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for kd-tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.OneDimensionBKDWriterCallback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a k-d tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this kd-tree in the overall SSTable kd-tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
Review Comment:
For variable names and such, I'm fine with "tree" as a prefix, tbh
It's not like it's unclear which tree we mean in this context.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#issuecomment-1589646211
@adelapena @maedhroz I have addressed the initial review comments but I will return to this tomorrow following the discussion on class and variable naming. Can we agree on the following:
- package name - `balancedtree`
- class name prefix - `BlockBalancedTree`
- variable name prefix - `tree`
Do they sound reasonable? If so, I'll get the remaining changes in tomorrow. The commit I have just made, removes any mention of kd/bkd from the source code (hopefully) but I haven't fully completed test yet.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228447241
##########
src/java/org/apache/cassandra/index/sai/metrics/MulticastQueryEventListeners.java:
##########
@@ -62,6 +67,79 @@ public QueryEventListener.PostingListEventListener postingListEventListener()
}
}
+ public static class Multicast2BKDIndexEventListener implements QueryEventListener.BlockBalancedTreeEventListener
Review Comment:
```suggestion
public static class Multicast2BBTIndexEventListener implements QueryEventListener.BlockBalancedTreeEventListener
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230196695
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/TraversingBlockBalancedTreeReader.java:
##########
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.index.sai.disk.io.IndexInputReader;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.io.util.RandomAccessReader;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.MathUtil;
+
+/**
+ * Base reader for a block balanced tree previously written with {@link BlockBalancedTreeWriter}.
+ * <p>
+ * Holds the index tree on heap and enables its traversal via {@link #traverse(TraversalCallback)}.
+ */
+public class TraversingBlockBalancedTreeReader implements Closeable
Review Comment:
```suggestion
public class BlockBalancedTreeWalker implements Closeable
```
Just a nit to satisfy my OCD ;)
Although it does kind of match up w/ what we do for tries...
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] mike-tr-adamson commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "mike-tr-adamson (via GitHub)" <gi...@apache.org>.
mike-tr-adamson commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228167733
##########
src/java/org/apache/cassandra/db/ClusteringComparator.java:
##########
@@ -353,6 +353,8 @@ public <V> Clustering<V> clusteringFromByteComparable(ValueAccessor<V> accessor,
{
ByteComparable.Version version = ByteComparable.Version.OSS50;
ByteSource.Peekable orderedBytes = ByteSource.peekable(comparable.asComparableBytes(version));
+ if (orderedBytes == null)
+ return null;
Review Comment:
I can answer that. We write `null` in the `PrimaryKey` if there is no clustering.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228470149
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/NumericIndexWriter.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.format.IndexComponent;
+import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Specialized writer for 1-dim point values, that builds them into a {@link BlockBalancedTreeWriter} with auxiliary
+ * posting lists on eligible tree levels.
+ *
+ * Given a sorted input {@link IntersectingPointValues}, the flush process is optimised because we don't need to
+ * buffer all point values to sort them.
+ */
+public class NumericIndexWriter
+{
+ public static final int MAX_POINTS_IN_LEAF_NODE = BlockBalancedTreeWriter.DEFAULT_MAX_POINTS_IN_LEAF_NODE;
+
+ private final BlockBalancedTreeWriter writer;
+ private final IndexDescriptor indexDescriptor;
+ private final IndexContext indexContext;
+ private final int bytesPerValue;
+
+ /**
+ * @param maxSegmentRowId maximum possible segment row ID, used to create `maxDoc` for the balanced tree
+ */
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ this(indexDescriptor, indexContext, MAX_POINTS_IN_LEAF_NODE, bytesPerValue, maxSegmentRowId);
+ }
+
+ @VisibleForTesting
+ public NumericIndexWriter(IndexDescriptor indexDescriptor,
+ IndexContext indexContext,
+ int maxPointsInLeafNode,
+ int bytesPerValue,
+ long maxSegmentRowId)
+ {
+ checkArgument(maxSegmentRowId >= 0, "[%s] maxRowId must be non-negative value, but got %s", indexContext.getIndexName(), maxSegmentRowId);
+
+ this.indexDescriptor = indexDescriptor;
+ this.indexContext = indexContext;
+ this.bytesPerValue = bytesPerValue;
+ this.writer = new BlockBalancedTreeWriter(maxSegmentRowId + 1, bytesPerValue, maxPointsInLeafNode);
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("bytesPerDim", bytesPerValue)
+ .add("bufferedPoints", writer.getPointCount())
+ .toString();
+ }
+
+ public static class LeafCallback implements BlockBalancedTreeWriter.Callback
+ {
+ final List<PackedLongValues> postings = new ArrayList<>();
+
+ public int numLeaves()
+ {
+ return postings.size();
+ }
+
+ @Override
+ public void writeLeafDocs(BlockBalancedTreeWriter.RowIDAndIndex[] sortedByRowID, int offset, int count)
+ {
+ PackedLongValues.Builder builder = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
+
+ for (int i = offset; i < count; ++i)
+ {
+ builder.add(sortedByRowID[i].rowID);
+ }
+ postings.add(builder.build());
+ }
+ }
+
+ /**
+ * Writes a balanced tree and posting lists from a {@link org.apache.lucene.codecs.MutablePointValues}.
+ *
+ * @param values points to write
+ *
+ * @return metadata describing the location and size of this balanced tree in the overall SSTable balanced tree component file
+ */
+ public SegmentMetadata.ComponentMetadataMap writeAll(IntersectingPointValues values) throws IOException
Review Comment:
nit/naming: Do we want to do the `writeAll()` -> `writeCompleteSegment()` renaming here (like we did for the other writer)?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228640850
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/IntersectingPointValues.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+
+import org.apache.lucene.codecs.MutablePointValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.BytesRef;
+
+public abstract class IntersectingPointValues extends MutablePointValues
+{
+ private static final byte[] EMPTY = new byte[0];
+
+ public boolean needsSorting()
+ {
+ return size() > 1;
+ }
+
+ public abstract void intersect(IntersectVisitor visitor) throws IOException;
+
+ @Override
+ public int getDocCount()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long size()
+ {
+ return 1;
+ }
+
+ @Override
+ public void getValue(int i, BytesRef packedValue)
+ {
+ // no-op
+ }
+
+ @Override
+ public byte getByteAt(int i, int k)
+ {
+ return 0;
+ }
+
+ @Override
+ public int getDocID(int i)
+ {
+ return 0;
+ }
+
+ @Override
+ public void swap(int i, int j)
+ {
+ throw new IllegalStateException("unexpected sorting");
+ }
+
+ @Override
+ public void intersect(PointValues.IntersectVisitor visitor)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long estimatePointCount(PointValues.IntersectVisitor visitor)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public byte[] getMinPackedValue()
+ {
+ return EMPTY;
+ }
+
+ @Override
+ public byte[] getMaxPackedValue()
+ {
+ return EMPTY;
+ }
+
+ @Override
+ public int getNumDimensions()
+ {
+ return 1;
+ }
+
+ @Override
+ public int getBytesPerDimension()
+ {
+ return 0;
+ }
+
+ public interface IntersectVisitor
+ {
+ /**
+ * Called for all documents in a leaf cell that crosses the query. The consumer should scrutinize the
Review Comment:
nit: documents -> rows?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228826431
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
+ public long writeField(IndexOutput out, IntersectingPointValues reader,
+ final Callback callback) throws IOException
+ {
+ SAICodecUtils.writeHeader(out);
+
+ // We are only ever dealing with one dimension, so we can sort the points in ascending order
+ // and write out the values
+ if (reader.needsSorting())
+ MutablePointsReaderUtils.sort(Math.toIntExact(maxDoc), bytesPerValue, reader, 0, Math.toIntExact(reader.size()));
+
+ OneDimensionBKDWriter oneDimWriter = new OneDimensionBKDWriter(out, callback);
+
+ reader.intersect((docID, packedValue) -> oneDimWriter.add(packedValue, docID));
+
+ long filePointer = oneDimWriter.finish();
+
+ SAICodecUtils.writeFooter(out);
+ return filePointer;
+ }
+
+ interface Callback
+ {
+ void writeLeafDocs(RowIDAndIndex[] leafDocs, int offset, int count);
+ }
+
+ public static class RowIDAndIndex
+ {
+ public int valueOrderIndex;
+ public long rowID;
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper(this)
+ .add("valueOrderIndex", valueOrderIndex)
+ .add("rowID", rowID)
+ .toString();
+ }
+ }
+
+ private class OneDimensionBKDWriter
Review Comment:
nit/naming: Perhaps we could call this `TreeBuilder` or something similar?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1228830151
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeWriter.java:
##########
@@ -0,0 +1,766 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import com.google.common.base.MoreObjects;
+
+import org.apache.cassandra.index.sai.disk.io.RAMIndexOutput;
+import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.IntroSorter;
+import org.apache.lucene.util.Sorter;
+import org.apache.lucene.util.bkd.MutablePointsReaderUtils;
+
+/**
+ * This is a specialisation of the lucene BKDWriter that only writes a single dimension
+ * balanced tree.
+ * <p>
+ * Recursively builds a block balanced tree to assign all incoming points to smaller
+ * and smaller rectangles (cells) until the number of points in a given
+ * rectangle is <= <code>maxPointsInLeafNode</code>. The tree is
+ * fully balanced, which means the leaf nodes will have between 50% and 100% of
+ * the requested <code>maxPointsInLeafNode</code>. Values that fall exactly
+ * on a cell boundary may be in either cell.
+ *
+ * <p>
+ * <b>NOTE</b>: This can write at most Integer.MAX_VALUE * <code>maxPointsInLeafNode</code> total points.
+ */
+public class BlockBalancedTreeWriter
+{
+ // Enable to check that values are added to the tree in correct order and within bounds
+ private static final boolean DEBUG = false;
+
+ // Default maximum number of point in each leaf block
+ public static final int DEFAULT_MAX_POINTS_IN_LEAF_NODE = 1024;
+
+ private final int bytesPerValue;
+ private final BytesRef scratchBytesRef1 = new BytesRef();
+ private final int maxPointsInLeafNode;
+ private final byte[] minPackedValue;
+ private final byte[] maxPackedValue;
+ private long pointCount;
+ private final long maxDoc;
+
+ public BlockBalancedTreeWriter(long maxDoc, int bytesPerValue, int maxPointsInLeafNode)
+ {
+ if (maxPointsInLeafNode <= 0)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be > 0; got " + maxPointsInLeafNode);
+ if (maxPointsInLeafNode > ArrayUtil.MAX_ARRAY_LENGTH)
+ throw new IllegalArgumentException("maxPointsInLeafNode must be <= ArrayUtil.MAX_ARRAY_LENGTH (= " +
+ ArrayUtil.MAX_ARRAY_LENGTH + "); got " + maxPointsInLeafNode);
+
+ this.maxPointsInLeafNode = maxPointsInLeafNode;
+ this.bytesPerValue = bytesPerValue;
+ this.maxDoc = maxDoc;
+
+ minPackedValue = new byte[bytesPerValue];
+ maxPackedValue = new byte[bytesPerValue];
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public int getMaxPointsInLeafNode()
+ {
+ return maxPointsInLeafNode;
+ }
+
+ /**
+ * Write the point values from a {@link IntersectingPointValues}. The points can be reordered before writing
+ * to disk and does not use transient disk for reordering.
+ */
Review Comment:
nit: Especially w/ the callback here, maybe a full JavaDoc w/ `@params` would be useful. In the general method description, perhaps summarize that we're writing the header, the tree itself, and then a footer. Then for `@return`, the file position of the...footer?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] adelapena commented on pull request #2409: Numeric on-disk index write and search
Posted by "adelapena (via GitHub)" <gi...@apache.org>.
adelapena commented on PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#issuecomment-1591482473
Thanks, this is looking fantastic. I have given it a quick, partial CI run here: https://app.circleci.com/pipelines/github/adelapena/cassandra/2957/workflows/9aa9e72b-3683-4ed5-b4e0-480c3de66228
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1230021860
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
Review Comment:
nit: This writes both the auxiliary postings and the leaf postings, correct?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227236188
##########
src/java/org/apache/cassandra/index/sai/disk/v1/postings/PostingsWriter.java:
##########
@@ -108,6 +108,11 @@ public PostingsWriter(IndexDescriptor indexDescriptor, IndexContext indexContext
this(indexDescriptor, indexContext, BLOCK_SIZE);
Review Comment:
nit: I think line 45 above needs a `<p>`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227278242
##########
src/java/org/apache/cassandra/index/sai/disk/v1/segment/SegmentBuilder.java:
##########
@@ -71,10 +73,48 @@
private ByteBuffer minTerm;
private ByteBuffer maxTerm;
+ final AbstractType<?> termComparator;
long totalBytesAllocated;
int rowCount = 0;
int maxSegmentRowId = -1;
+ public static class KDTreeSegmentBuilder extends SegmentBuilder
Review Comment:
nit/TODO: Replace `KDTreeX` w/ whatever we land on for naming
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1227274061
##########
test/unit/org/apache/cassandra/index/sai/cql/BaseDataModel.java:
##########
@@ -167,6 +167,12 @@ public void createTables(Executor tester)
tester.createTable(String.format(template, KEYSPACE, nonIndexedTable, keyColumnDefs, normalColumnDefs, primaryKey));
}
+ public void truncateTables(Executor tester) throws Throwable
+ {
+ executeLocal(tester, "TRUNCATE TABLE %s");
+ executeLocal(tester, "TRUNCATE TABLE %s");
Review Comment:
nit: Duplicate `TRUNCATE` calls?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232816860
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
Review Comment:
nit: Also mention that this could just be a leaf?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1232841037
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreePostingsWriter.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PackedLongsPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedLongValues;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Writes auxiliary posting lists for bbtree nodes. If a node has a posting list attached, it will contain every row
+ * id from all leaves reachable from that node.
+ * <p>
+ * Writer is stateful, because it needs to collect data from the balanced tree data structure first to find set of eligible
+ * nodes and leaf nodes reachable from them.
+ * <p>
+ * This is an optimised writer for 1-dim points, where we know that leaf blocks are written in value order (in this
+ * order we pass them to the {@link BlockBalancedTreeWriter}). That allows us to skip reading the leaves, instead
+ * just order leaf blocks by their offset in the index file, and correlate them with buffered posting lists.
+ */
+public class BlockBalancedTreePostingsWriter implements TraversingBlockBalancedTreeReader.TraversalCallback
+{
+ private static final Logger logger = LoggerFactory.getLogger(BlockBalancedTreePostingsWriter.class);
+
+ /**
+ * Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list.
+ */
+ public static final int MINIMUM_POSTINGS_LEAVES = CassandraRelevantProperties.SAI_MINIMUM_POSTINGS_LEAVES.getInt();
+ /**
+ * Skip, or the sampling interval, for selecting a balanced tree level that is eligible for an auxiliary posting list.
+ * Sampling starts from 0, but the balanced tree root node is at level 1. For skip = 4, eligible levels are 4, 8, 12, etc. (no
+ * level 0, because there is no node at level 0).
+ */
+ public static final int POSTINGS_SKIP = CassandraRelevantProperties.SAI_POSTINGS_SKIP.getInt();
+
+ private final List<PackedLongValues> postings;
+ private final TreeMap<Long, Integer> leafOffsetToNodeID = new TreeMap<>(Long::compareTo);
+ private final Multimap<Integer, Integer> nodeToChildLeaves = HashMultimap.create();
+
+ private final IndexContext indexContext;
+ int numNonLeafPostings = 0;
+ int numLeafPostings = 0;
+
+ BlockBalancedTreePostingsWriter(List<PackedLongValues> postings, IndexContext indexContext)
+ {
+ this.postings = postings;
+ this.indexContext = indexContext;
+ }
+
+ @Override
+ public void onLeaf(int leafNodeID, long leafBlockFP, IntArrayList pathToRoot)
+ {
+ checkArgument(!pathToRoot.containsInt(leafNodeID));
+ checkArgument(pathToRoot.isEmpty() || leafNodeID > pathToRoot.get(pathToRoot.size() - 1));
+
+ leafOffsetToNodeID.put(leafBlockFP, leafNodeID);
+ for (int i = 0; i < pathToRoot.size(); i++)
+ {
+ int level = i + 1;
+ if (isLevelEligibleForPostingList(level))
+ {
+ int nodeID = pathToRoot.get(i);
+ nodeToChildLeaves.put(nodeID, leafNodeID);
+ }
+ }
+ }
+
+ public long finish(IndexOutput out) throws IOException
+ {
+ checkState(postings.size() == leafOffsetToNodeID.size(),
+ "Expected equal number of postings lists (%s) and leaf offsets (%s).",
+ postings.size(), leafOffsetToNodeID.size());
+
+ try (PostingsWriter postingsWriter = new PostingsWriter(out))
+ {
+ Iterator<PackedLongValues> postingsIterator = postings.iterator();
+ Map<Integer, PackedLongValues> leafToPostings = new HashMap<>();
+ leafOffsetToNodeID.forEach((fp, nodeID) -> leafToPostings.put(nodeID, postingsIterator.next()));
+
+ long postingsRamBytesUsed = postings.stream()
+ .mapToLong(PackedLongValues::ramBytesUsed)
+ .sum();
+
+ List<Integer> internalNodeIDs = nodeToChildLeaves.keySet()
+ .stream()
+ .filter(i -> nodeToChildLeaves.get(i).size() >= MINIMUM_POSTINGS_LEAVES)
+ .collect(Collectors.toList());
+
+ Collection<Integer> leafNodeIDs = leafOffsetToNodeID.values();
+
+ logger.debug(indexContext.logMessage("Writing posting lists for {} internal and {} leaf balanced tree nodes. Leaf postings memory usage: {}."),
+ internalNodeIDs.size(),
+ leafNodeIDs.size(),
+ FBUtilities.prettyPrintMemory(postingsRamBytesUsed));
+
+ long startFP = out.getFilePointer();
+ Stopwatch flushTime = Stopwatch.createStarted();
+ TreeMap<Integer, Long> nodeIDToPostingsFilePointer = new TreeMap<>();
+ for (int nodeID : Iterables.concat(internalNodeIDs, leafNodeIDs))
+ {
+ Collection<Integer> leaves = nodeToChildLeaves.get(nodeID);
+
+ if (leaves.isEmpty())
+ {
+ leaves = Collections.singletonList(nodeID);
+ numLeafPostings++;
+ }
+ else
+ {
+ numNonLeafPostings++;
+ }
+
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, Comparator.comparingLong(PeekablePostingList::peek));
Review Comment:
What if instead of creating this w/ an arbitrary starting size of 100 on every iteration, we create the `PriorityQueue` outside the loop w/ a starting size of `MINIMUM_POSTINGS_LEAVES` and just `clear()` it at the end of every iteration?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org
[GitHub] [cassandra] maedhroz commented on a diff in pull request #2409: Numeric on-disk index write and search
Posted by "maedhroz (via GitHub)" <gi...@apache.org>.
maedhroz commented on code in PR #2409:
URL: https://github.com/apache/cassandra/pull/2409#discussion_r1234532622
##########
src/java/org/apache/cassandra/index/sai/disk/v1/bbtree/BlockBalancedTreeReader.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.cassandra.index.sai.disk.v1.bbtree;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.QueryCancelledException;
+import org.apache.cassandra.index.sai.IndexContext;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
+import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
+import org.apache.cassandra.index.sai.disk.v1.postings.FilteringPostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.MergePostingList;
+import org.apache.cassandra.index.sai.disk.v1.postings.PostingsReader;
+import org.apache.cassandra.index.sai.metrics.QueryEventListener;
+import org.apache.cassandra.index.sai.postings.PeekablePostingList;
+import org.apache.cassandra.index.sai.postings.PostingList;
+import org.apache.cassandra.io.util.FileHandle;
+import org.apache.cassandra.io.util.FileUtils;
+import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.packed.DirectWriter;
+
+/**
+ * Handles intersection of a point or point range with a block balanced tree previously written with
+ * {@link BlockBalancedTreeWriter}.
+ */
+public class BlockBalancedTreeReader extends TraversingBlockBalancedTreeReader implements Closeable
+{
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final Comparator<PeekablePostingList> COMPARATOR = Comparator.comparingLong(PeekablePostingList::peek);
+
+ private final IndexContext indexContext;
+ private final FileHandle postingsFile;
+ private final BlockBalancedTreePostingsIndex postingsIndex;
+ private final DirectReaders.Reader leafOrderMapReader;
+
+ /**
+ * Performs a blocking read.
+ */
+ public BlockBalancedTreeReader(IndexContext indexContext,
+ FileHandle treeIndexFile,
+ long treeIndexRoot,
+ FileHandle postingsFile,
+ long treePostingsRoot) throws IOException
+ {
+ super(treeIndexFile, treeIndexRoot);
+ this.indexContext = indexContext;
+ this.postingsFile = postingsFile;
+ this.postingsIndex = new BlockBalancedTreePostingsIndex(postingsFile, treePostingsRoot);
+ leafOrderMapReader = DirectReaders.getReaderForBitsPerValue((byte) DirectWriter.unsignedBitsRequired(maxPointsInLeafNode - 1));
+ }
+
+ public int getBytesPerValue()
+ {
+ return bytesPerValue;
+ }
+
+ public long getPointCount()
+ {
+ return pointCount;
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ FileUtils.closeQuietly(postingsFile);
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ public PostingList intersect(IntersectVisitor visitor, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ Relation relation = visitor.compare(minPackedValue, maxPackedValue);
+
+ if (relation == Relation.CELL_OUTSIDE_QUERY)
+ {
+ listener.onIntersectionEarlyExit();
+ return null;
+ }
+
+ listener.onSegmentHit();
+ IndexInput treeInput = IndexFileUtils.instance.openInput(treeIndexFile);
+ IndexInput postingsInput = IndexFileUtils.instance.openInput(postingsFile);
+ IndexInput postingsSummaryInput = IndexFileUtils.instance.openInput(postingsFile);
+ PackedIndexTree index = new PackedIndexTree();
+
+ Intersection intersection = relation == Relation.CELL_INSIDE_QUERY
+ ? new Intersection(treeInput, postingsInput, postingsSummaryInput, index, listener, context)
+ : new FilteringIntersection(treeInput, postingsInput, postingsSummaryInput, index, visitor, listener, context);
+
+ return intersection.execute();
+ }
+
+ /**
+ * Synchronous intersection of a point or point range with a block balanced tree previously written
+ * with {@link BlockBalancedTreeWriter}.
+ */
+ private class Intersection
+ {
+ private final Stopwatch queryExecutionTimer = Stopwatch.createStarted();
+ final QueryContext context;
+
+ final IndexInput treeInput;
+ final IndexInput postingsInput;
+ final IndexInput postingsSummaryInput;
+ final PackedIndexTree index;
+ final QueryEventListener.BalancedTreeEventListener listener;
+
+ Intersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ this.treeInput = treeInput;
+ this.postingsInput = postingsInput;
+ this.postingsSummaryInput = postingsSummaryInput;
+ this.index = index;
+ this.listener = listener;
+ this.context = context;
+ }
+
+ public PostingList execute()
+ {
+ try
+ {
+ PriorityQueue<PeekablePostingList> postingLists = new PriorityQueue<>(100, COMPARATOR);
+
+ executeInternal(postingLists);
+
+ FileUtils.closeQuietly(treeInput);
+
+ return mergePostings(postingLists);
+ }
+ catch (Throwable t)
+ {
+ if (!(t instanceof QueryCancelledException))
+ logger.error(indexContext.logMessage("Balanced tree intersection failed on {}"), treeIndexFile.path(), t);
+
+ closeOnException();
+ throw Throwables.cleaned(t);
+ }
+ }
+
+ protected void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists);
+ }
+
+ protected void closeOnException()
+ {
+ FileUtils.closeQuietly(treeInput);
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ }
+
+ protected PostingList mergePostings(PriorityQueue<PeekablePostingList> postingLists)
+ {
+ final long elapsedMicros = queryExecutionTimer.stop().elapsed(TimeUnit.MICROSECONDS);
+
+ listener.onIntersectionComplete(elapsedMicros, TimeUnit.MICROSECONDS);
+ listener.postingListsHit(postingLists.size());
+
+ if (postingLists.isEmpty())
+ {
+ FileUtils.closeQuietly(postingsInput);
+ FileUtils.closeQuietly(postingsSummaryInput);
+ return null;
+ }
+ else
+ {
+ if (logger.isTraceEnabled())
+ logger.trace(indexContext.logMessage("[{}] Intersection completed in {} microseconds. {} leaf and internal posting lists hit."),
+ treeIndexFile.path(), elapsedMicros, postingLists.size());
+ return MergePostingList.merge(postingLists, () -> FileUtils.close(postingsInput, postingsSummaryInput));
+ }
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ context.checkpoint();
+
+ final int nodeID = index.getNodeID();
+
+ // if there is pre-built posting for entire subtree
+ if (postingsIndex.exists(nodeID))
+ {
+ postingLists.add(initPostingReader(postingsIndex.getPostingsFilePointer(nodeID)));
+ return;
+ }
+
+ Preconditions.checkState(!index.isLeafNode(), "Leaf node %s does not have balanced tree postings.", index.getNodeID());
+
+ // Recurse on left subtree:
+ index.pushLeft();
+ collectPostingLists(postingLists);
+ index.pop();
+
+ // Recurse on right subtree:
+ index.pushRight();
+ collectPostingLists(postingLists);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initPostingReader(long offset) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ return PeekablePostingList.makePeekable(new PostingsReader(postingsInput, summary, listener.postingListEventListener()));
+ }
+ }
+
+ private class FilteringIntersection extends Intersection
+ {
+ private final IntersectVisitor visitor;
+ private final byte[] packedValue;
+ private final short[] origIndex;
+
+ FilteringIntersection(IndexInput treeInput, IndexInput postingsInput, IndexInput postingsSummaryInput,
+ PackedIndexTree index, IntersectVisitor visitor,
+ QueryEventListener.BalancedTreeEventListener listener, QueryContext context)
+ {
+ super(treeInput, postingsInput, postingsSummaryInput, index, listener, context);
+ this.visitor = visitor;
+ this.packedValue = new byte[packedBytesLength];
+ this.origIndex = new short[maxPointsInLeafNode];
+ }
+
+ @Override
+ public void executeInternal(final PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ collectPostingLists(postingLists, minPackedValue, maxPackedValue);
+ }
+
+ private void collectPostingLists(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ context.checkpoint();
+
+ final Relation r = visitor.compare(cellMinPacked, cellMaxPacked);
+
+ if (r == Relation.CELL_OUTSIDE_QUERY)
+ {
+ // This cell is fully outside the query shape: stop recursing
+ return;
+ }
+
+ if (r == Relation.CELL_INSIDE_QUERY)
+ {
+ // This cell is fully inside the query shape: recursively add all points in this cell without filtering
+ super.collectPostingLists(postingLists);
+ return;
+ }
+
+ if (index.isLeafNode())
+ {
+ if (index.nodeExists())
+ filterLeaf(postingLists);
+ return;
+ }
+
+ visitNode(postingLists, cellMinPacked, cellMaxPacked);
+ }
+
+ private void filterLeaf(PriorityQueue<PeekablePostingList> postingLists) throws IOException
+ {
+ treeInput.seek(index.getLeafBlockFP());
+
+ int count = treeInput.readVInt();
+
+ // loading doc ids occurred here prior
+
+ int orderMapLength = treeInput.readVInt();
+
+ long orderMapPointer = treeInput.getFilePointer();
+
+ SeekingRandomAccessInput randomAccessInput = new SeekingRandomAccessInput(treeInput);
+ for (int x = 0; x < count; x++)
+ {
+ origIndex[x] = (short) LeafOrderMap.getValue(randomAccessInput, orderMapPointer, x, leafOrderMapReader);
+ }
+
+ // seek beyond the ordermap
+ treeInput.seek(orderMapPointer + orderMapLength);
+
+ FixedBitSet fixedBitSet = visitDocValues(treeInput, count, visitor, origIndex);
+
+ int nodeID = index.getNodeID();
+
+ if (postingsIndex.exists(nodeID) && fixedBitSet.cardinality() > 0)
+ {
+ long pointer = postingsIndex.getPostingsFilePointer(nodeID);
+ postingLists.add(initFilteringPostingReader(pointer, fixedBitSet));
+ }
+ }
+
+ void visitNode(PriorityQueue<PeekablePostingList> postingLists, byte[] cellMinPacked, byte[] cellMaxPacked) throws IOException
+ {
+ byte[] splitPackedValue = index.getSplitPackedValue();
+ BytesRef splitDimValue = index.getSplitDimValue();
+ assert splitDimValue.length == bytesPerValue;
+
+ // make sure cellMin <= splitValue <= cellMax:
+ assert FutureArrays.compareUnsigned(cellMinPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) <= 0 : "bytesPerValue=" + bytesPerValue;
+ assert FutureArrays.compareUnsigned(cellMaxPacked, 0, bytesPerValue, splitDimValue.bytes, splitDimValue.offset, splitDimValue.offset + bytesPerValue) >= 0 : "bytesPerValue=" + bytesPerValue;
+
+ // Recurse on left subtree:
+ System.arraycopy(cellMaxPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+
+ index.pushLeft();
+ collectPostingLists(postingLists, cellMinPacked, splitPackedValue);
+ index.pop();
+
+ // Restore the split dim value since it may have been overwritten while recursing:
+ System.arraycopy(splitPackedValue, 0, splitDimValue.bytes, splitDimValue.offset, bytesPerValue);
+ // Recurse on right subtree:
+ System.arraycopy(cellMinPacked, 0, splitPackedValue, 0, packedBytesLength);
+ System.arraycopy(splitDimValue.bytes, splitDimValue.offset, splitPackedValue, 0, bytesPerValue);
+ index.pushRight();
+ collectPostingLists(postingLists, splitPackedValue, cellMaxPacked);
+ index.pop();
+ }
+
+ @SuppressWarnings({"resource", "RedundantSuppression"})
+ private PeekablePostingList initFilteringPostingReader(long offset, FixedBitSet filter) throws IOException
+ {
+ final PostingsReader.BlocksSummary summary = new PostingsReader.BlocksSummary(postingsSummaryInput, offset);
+ PostingsReader postingsReader = new PostingsReader(postingsInput, summary, listener.postingListEventListener());
+ return PeekablePostingList.makePeekable(new FilteringPostingList(filter, postingsReader));
+ }
+
+ private FixedBitSet visitDocValues(IndexInput in, int count, IntersectVisitor visitor, short[] origIndex) throws IOException
+ {
+ int commonPrefixLength = readCommonPrefixLength(in);
+ int compressedDimension = readCompressedDimension(in);
+ // If the compressed dimension is -1 it means that all values in the block are equal
+ if (compressedDimension == -1)
Review Comment:
...in other words, could this just be: `if (commonPrefixLength == bytesPerValue)` ?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org