Github user tokee commented on a diff in the pull request:

    https://github.com/apache/lucene-solr/pull/525#discussion_r245993566
  
    --- Diff: 
lucene/core/src/java/org/apache/lucene/codecs/lucene80/IndexedDISI.java ---
    @@ -0,0 +1,601 @@
    +/*
    + * 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.lucene.codecs.lucene80;
    +
    +import java.io.DataInput;
    +import java.io.IOException;
    +
    +import org.apache.lucene.search.DocIdSetIterator;
    +import org.apache.lucene.store.IndexInput;
    +import org.apache.lucene.store.IndexOutput;
    +import org.apache.lucene.store.RandomAccessInput;
    +import org.apache.lucene.util.ArrayUtil;
    +import org.apache.lucene.util.BitSetIterator;
    +import org.apache.lucene.util.FixedBitSet;
    +import org.apache.lucene.util.RoaringDocIdSet;
    +
    +/**
    + * Disk-based implementation of a {@link DocIdSetIterator} which can return
    + * the index of the current document, i.e. the ordinal of the current 
document
    + * among the list of documents that this iterator can return. This is 
useful
    + * to implement sparse doc values by only having to encode values for 
documents
    + * that actually have a value.
    + * <p>Implementation-wise, this {@link DocIdSetIterator} is inspired of
    + * {@link RoaringDocIdSet roaring bitmaps} and encodes ranges of {@code 
65536}
    + * documents independently and picks between 3 encodings depending on the
    + * density of the range:<ul>
    + *   <li>{@code ALL} if the range contains 65536 documents exactly,
    + *   <li>{@code DENSE} if the range contains 4096 documents or more; in 
that
    + *       case documents are stored in a bit set,
    + *   <li>{@code SPARSE} otherwise, and the lower 16 bits of the doc IDs are
    + *       stored in a {@link DataInput#readShort() short}.
    + * </ul>
    + * <p>Only ranges that contain at least one value are encoded.
    + * <p>This implementation uses 6 bytes per document in the worst-case, 
which happens
    + * in the case that all ranges contain exactly one document.
    + *
    + * 
    + * To avoid O(n) lookup time complexity, with n being the number of 
documents, two lookup
    + * tables are used: A lookup table for block offset and index, and a rank 
structure
    + * for DENSE block index lookups.
    + *
    + * The lookup table is an array of {@code int}-pairs, with a pair for each 
block. It allows for
    + * direct jumping to the block, as opposed to iteration from the current 
position and forward
    + * one block at a time.
    + *
    + * Each int-pair entry consists of 2 logical parts:
    + *
    + * The first 32 bit int holds the index (number of set bits in the blocks) 
up to just before the
    + * wanted block. The maximum number of set bits is the maximum number of 
documents, which is < 2^31.
    + *
    + * The next int holds the offset in bytes into the underlying slice. As 
there is a maximum of 2^16
    + * blocks, it follows that the maximum size of any block must not exceed 
2^15 bytes to avoid
    + * overflow (2^16 bytes if the int is treated as unsigned). This is 
currently the case, with the
    + * largest block being DENSE and using 2^13 + 36 bytes.
    + *
    + * The cache overhead is numDocs/1024 bytes.
    + *
    + * Note: There are 4 types of blocks: ALL, DENSE, SPARSE and non-existing 
(0 set bits).
    + * In the case of non-existing blocks, the entry in the lookup table has 
index equal to the
    + * previous entry and offset equal to the next non-empty block.
    + *
    + * The block lookup table is stored at the end of the total block 
structure.
    + *
    + *
    + * The rank structure for DENSE blocks is an array of byte-pairs with an 
entry for each
    + * sub-block (default 512 bits) out of the 65536 bits in the outer DENSE 
block.
    + *
    + * Each rank-entry states the number of set bits within the block up to 
the bit before the
    + * bit positioned at the start of the sub-block.
    + * Note that that the rank entry of the first sub-block is always 0 and 
that the last entry can
    + * at most be 65536-2 = 65634 and thus will always fit into an byte-pair 
of 16 bits.
    + *
    + * The rank structure for a given DENSE block is stored at the beginning 
of the DENSE block.
    + * This ensures locality and keeps logistics simple.
    + *
    + * @lucene.internal
    + */
    +final class IndexedDISI extends DocIdSetIterator {
    +
    +  // jump-table time/space trade-offs to consider:
    +  // The block offsets and the block indexes could be stored in more 
compressed form with
    +  // two PackedInts or two MonotonicDirectReaders.
    +  // The DENSE ranks (default 128 shorts = 256 bytes) could likewise be 
compressed. But as there is
    +  // at least 4096 set bits in DENSE blocks, there will be at least one 
rank with 2^12 bits, so it
    +  // is doubtful if there is much to gain here.
    +  
    +  private static final int BLOCK_SIZE = 65536;   // The number of docIDs 
that a single block represents
    +
    +  private static final int DENSE_BLOCK_LONGS = BLOCK_SIZE/Long.SIZE; // 
1024
    +  public static final byte DEFAULT_DENSE_RANK_POWER = 9; // Every 512 
docIDs / 8 longs
    +
    +  static final int MAX_ARRAY_LENGTH = (1 << 12) - 1;
    +
    +  private static void flush(
    +      int block, FixedBitSet buffer, int cardinality, byte denseRankPower, 
IndexOutput out) throws IOException {
    +    assert block >= 0 && block < 65536;
    +    out.writeShort((short) block);
    +    assert cardinality > 0 && cardinality <= 65536;
    +    out.writeShort((short) (cardinality - 1));
    +    if (cardinality > MAX_ARRAY_LENGTH) {
    +      if (cardinality != 65536) { // all docs are set
    +        if (denseRankPower != -1) {
    +          final byte[] rank = createRank(buffer, denseRankPower);
    +          out.writeBytes(rank, rank.length);
    +        }
    +        for (long word : buffer.getBits()) {
    +          out.writeLong(word);
    +        }
    +      }
    +    } else {
    +      BitSetIterator it = new BitSetIterator(buffer, cardinality);
    +      for (int doc = it.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; 
doc = it.nextDoc()) {
    +        out.writeShort((short) doc);
    +      }
    +    }
    +  }
    +
    +  // Creates a DENSE rank-entry (the number of set bits up to a given 
point) for the buffer.
    +  // One rank-entry for every {@code 2^denseRankPower} bits, with each 
rank-entry using 2 bytes.
    +  // Represented as a byte[] for fast flushing and mirroring of the 
retrieval representation.
    +  private static byte[] createRank(FixedBitSet buffer, byte 
denseRankPower) {
    +    final int longsPerRank = 1 << (denseRankPower-6);
    +    final int rankMark = longsPerRank-1;
    +    final int rankIndexShift = denseRankPower-7; // 6 for the long (2^6) + 
1 for 2 bytes/entry
    +    final byte[] rank = new byte[DENSE_BLOCK_LONGS >> rankIndexShift];
    +    final long[] bits = buffer.getBits();
    +    int bitCount = 0;
    +    for (int word = 0 ; word < DENSE_BLOCK_LONGS ; word++) {
    +      if ((word & rankMark) == 0) { // Every longsPerRank longs
    +        rank[word >> rankIndexShift] = (byte)(bitCount>>8);
    +        rank[(word >> rankIndexShift)+1] = (byte)(bitCount & 0xFF);
    +      }
    +      bitCount += Long.bitCount(bits[word]);
    +    }
    +    return rank;
    +  }
    +
    +  /**
    +   * Writes the docIDs from it to out, in logical blocks, one for each 
65536 docIDs in monotonically increasing
    +   * gap-less order. DENSE blocks uses {@link #DEFAULT_DENSE_RANK_POWER} 
of 9 (every 512 docIDs / 8 longs).
    +   * The caller must keep track of the number of jump-table entries 
(returned by this method) as well as the
    +   * denseRankPower (9 for this method) and provide them when constructing 
an IndexedDISI for reading.
    +   * @param it  the document IDs.
    +   * @param out destination for the blocks.
    +   * @throws IOException if there was an error writing to out.
    +   * @return the number of jump-table entries following the blocks, -1 for 
no entries.
    +   *         This should be stored in meta and used when creating an 
instance of IndexedDISI.
    +   */
    +  static short writeBitSet(DocIdSetIterator it, IndexOutput out) throws 
IOException {
    +    return writeBitSet(it, out, DEFAULT_DENSE_RANK_POWER);
    +  }
    +
    +  /**
    +   * Writes the docIDs from it to out, in logical blocks, one for each 
65536 docIDs in monotonically
    +   * increasing gap-less order.
    +   * The caller must keep track of the number of jump-table entries 
(returned by this method) as well as the
    +   * denseRankPower and provide them when constructing an IndexedDISI for 
reading.
    +   * @param it  the document IDs.
    +   * @param out destination for the blocks.
    +   * @param denseRankPower for {@link Method#DENSE} blocks, a rank will be 
written every {@code 2^denseRankPower} docIDs.
    +   *                       Values &lt; 7 (every 128 docIDs) or &gt; 15 
(every 32768 docIDs) disables DENSE rank.
    +   *                       Recommended values are 8-12: Every 256-4096 
docIDs or 4-64 longs.
    +   *                       {@link #DEFAULT_DENSE_RANK_POWER} is 9: Every 
512 docIDs.
    +   *                       This should be stored in meta and used when 
creating an instance of IndexedDISI.
    +   * @throws IOException if there was an error writing to out.
    +   * @return the number of jump-table entries following the blocks, -1 for 
no entries.
    +   *         This should be stored in meta and used when creating an 
instance of IndexedDISI.
    +   */
    +  static short writeBitSet(DocIdSetIterator it, IndexOutput out, byte 
denseRankPower) throws IOException {
    +    final long origo = out.getFilePointer(); // All jumps are relative to 
the origo
    +    if (denseRankPower < 7 || denseRankPower > 15) {
    +      denseRankPower = -1; // Disabled
    +    }
    +    int totalCardinality = 0;
    +    int blockCardinality = 0;
    +    final FixedBitSet buffer = new FixedBitSet(1<<16);
    +    int[] jumps = new int[ArrayUtil.oversize(1, Integer.BYTES*2)];
    +    int prevBlock = -1;
    +    int jumpBlockIndex = 0;
    +
    +    for (int doc = it.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; doc 
= it.nextDoc()) {
    +      final int block = doc >>> 16;
    +      if (prevBlock != -1 && block != prevBlock) {
    +        // Track offset+index from previous block up to current
    +        jumps = addJumps(jumps, out.getFilePointer()-origo, 
totalCardinality, jumpBlockIndex, prevBlock+1);
    +        jumpBlockIndex = prevBlock+1;
    +        // Flush block
    +        flush(prevBlock, buffer, blockCardinality, denseRankPower, out);
    +        // Reset for next block
    +        buffer.clear(0, buffer.length());
    +        totalCardinality += blockCardinality;
    +        blockCardinality = 0;
    +      }
    +      buffer.set(doc & 0xFFFF);
    +      blockCardinality++;
    +      prevBlock = block;
    +    }
    +    if (blockCardinality > 0) {
    +      jumps = addJumps(jumps, out.getFilePointer()-origo, 
totalCardinality, jumpBlockIndex, prevBlock+1);
    +      flush(prevBlock, buffer, blockCardinality, denseRankPower, out);
    +      buffer.clear(0, buffer.length());
    +      prevBlock++;
    +    }
    +    final int lastBlock = prevBlock == -1 ? 0 : prevBlock; // There will 
always be at least 1 block (NO_MORE_DOCS)
    +    // Last entry is a SPARSE with blockIndex == 32767 and the single 
entry 65535, which becomes the docID NO_MORE_DOCS
    +    // To avoid creating 65K jump-table entries, only a single entry is 
created pointing to the offset of the
    +    // NO_MORE_DOCS block, but with the jumpBlockIndex set to the logical 
EMPTY block after all real blocks.
    +    jumps = addJumps(jumps, out.getFilePointer()-origo, 
DocIdSetIterator.NO_MORE_DOCS & 0xFFFF0000,
    +        lastBlock, lastBlock+1);
    +    buffer.set(DocIdSetIterator.NO_MORE_DOCS & 0xFFFF);
    +    flush(DocIdSetIterator.NO_MORE_DOCS >>> 16, buffer, 1, denseRankPower, 
out);
    +    // offset+index jump-table stored at the end
    +    return flushBlockJumps(jumps, lastBlock+1, out, origo);
    +  }
    +
    +  // Adds entries to the offset & index jump-table for blocks
    +  private static int[] addJumps(int[] jumps, long offset, int index, int 
startBlock, int endBlock) {
    +    assert offset < Integer.MAX_VALUE : "Logically the offset should not 
exceed 2^30 but was >= Integer.MAX_VALUE";
    +    jumps = ArrayUtil.grow(jumps, (endBlock+1)*2);
    +    for (int b = startBlock; b < endBlock; b++) {
    +      jumps[b*2] = index;
    +      jumps[b*2+1] = (int) offset;
    +    }
    +    return jumps;
    +  }
    +
    +  // Flushes the offet & index jump-table for blocks. This should be the 
last data written to out
    +  // This method returns the blockCount for the blocks reachable for the 
jump_table or -1 for no jump-table
    +  private static short flushBlockJumps(int[] jumps, int blockCount, 
IndexOutput out, long origo) throws IOException {
    +    if (blockCount == 2) { // Jumps with a single real entry + 
NO_MORE_DOCS is just wasted space so we ignore that
    +      blockCount = 0;
    +    }
    +    for (int i = 0 ; i < blockCount ; i++) {
    +      out.writeInt(jumps[i*2]); // index
    +      out.writeInt(jumps[i*2+1]); // offset
    +    }
    +    // As there are at most 32k blocks, the count is a short
    +    // The jumpTableOffset will be at lastPos - (blockCount * Long.BYTES)
    +    return (short)blockCount;
    +  }
    +
    +  /** The slice that stores the {@link DocIdSetIterator}. */
    +  private final IndexInput slice;
    +  private final int jumpTableEntryCount;
    +  private final byte denseRankPower;
    +  private final RandomAccessInput jumpTable; // Skip blocks of 64K bits
    +  private final byte[] denseRankTable;
    +  private final long cost;
    +
    +  /**
    +   * @param in backing data.
    +   * @param offset starting offset for blocks in backing data.
    +   * @param length the number of bytes in the backing data.
    +   * @param jumpTableEntryCount the number of blocks convered by the 
jump-table.
    +   * @param cost normally the number of logical docIDs.
    +   */
    +  IndexedDISI(IndexInput in, long offset, long length, int 
jumpTableEntryCount, byte denseRankPower, long cost) throws IOException {
    +    this(in.slice("docs", offset, length),
    --- End diff --
    
    Doing so makes the separation cleaner. Nice catch. I have implemented it.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@lucene.apache.org
For additional commands, e-mail: dev-h...@lucene.apache.org

Reply via email to