From 87ba4a12584a1ce753bb0dcc76ec0781943b1d28 Mon Sep 17 00:00:00 2001
From: Michael Busch
Date: Thu, 31 May 2007 07:48:53 +0000
Subject: [PATCH] LUCENE-866: Adds multi-level skip lists to the posting lists.
git-svn-id: https://svn.apache.org/repos/asf/lucene/java/trunk@543076 13f79535-47bb-0310-9956-ffa450edef68
---
CHANGES.txt | 6 +
.../lucene/index/DefaultSkipListReader.java | 114 ++++++++
.../lucene/index/DefaultSkipListWriter.java | 124 ++++++++
.../index/MultiLevelSkipListReader.java | 271 ++++++++++++++++++
.../index/MultiLevelSkipListWriter.java | 151 ++++++++++
.../apache/lucene/index/SegmentMerger.java | 87 +-----
.../apache/lucene/index/SegmentTermDocs.java | 111 +++----
.../apache/lucene/index/SegmentTermEnum.java | 12 +-
.../apache/lucene/index/TermInfosReader.java | 6 +-
.../apache/lucene/index/TermInfosWriter.java | 8 +-
.../content/xdocs/fileformats.xml | 39 ++-
.../lucene/index/TestMultiLevelSkipList.java | 154 ++++++++++
12 files changed, 917 insertions(+), 166 deletions(-)
create mode 100644 src/java/org/apache/lucene/index/DefaultSkipListReader.java
create mode 100644 src/java/org/apache/lucene/index/DefaultSkipListWriter.java
create mode 100644 src/java/org/apache/lucene/index/MultiLevelSkipListReader.java
create mode 100644 src/java/org/apache/lucene/index/MultiLevelSkipListWriter.java
create mode 100644 src/test/org/apache/lucene/index/TestMultiLevelSkipList.java
diff --git a/CHANGES.txt b/CHANGES.txt
index fe8a5ab7213..25e11bf013e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -209,6 +209,12 @@ Optimizations
BufferedIndexInput, but only when used during merging. Together,
these increases yield 10-18% overall performance gain vs the
previous 1K defaults. (Mike McCandless)
+
+ 7. LUCENE-866: Adds multi-level skip lists to the posting lists. This speeds
+ up most queries that use skipTo(), especially on big indexes with large posting
+ lists. For average AND queries the speedup is about 20%, for queries that
+ contain very frequence and very unique terms the speedup can be over 80%.
+ (Michael Busch)
Documentation
diff --git a/src/java/org/apache/lucene/index/DefaultSkipListReader.java b/src/java/org/apache/lucene/index/DefaultSkipListReader.java
new file mode 100644
index 00000000000..5d6f48b21f0
--- /dev/null
+++ b/src/java/org/apache/lucene/index/DefaultSkipListReader.java
@@ -0,0 +1,114 @@
+package org.apache.lucene.index;
+
+/**
+ * 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.
+ */
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.lucene.store.IndexInput;
+
+/**
+ * Implements the skip list reader for the default posting list format
+ * that stores positions and payloads.
+ *
+ */
+public class DefaultSkipListReader extends MultiLevelSkipListReader {
+ private boolean currentFieldStoresPayloads;
+ private long freqPointer[];
+ private long proxPointer[];
+ private int payloadLength[];
+
+ private long lastFreqPointer;
+ private long lastProxPointer;
+ private int lastPayloadLength;
+
+
+ DefaultSkipListReader(IndexInput skipStream, int maxSkipLevels, int skipInterval) {
+ super(skipStream, maxSkipLevels, skipInterval);
+ freqPointer = new long[maxSkipLevels];
+ proxPointer = new long[maxSkipLevels];
+ payloadLength = new int[maxSkipLevels];
+ }
+
+ void init(long skipPointer, long freqBasePointer, long proxBasePointer, int df, boolean storesPayloads) {
+ super.init(skipPointer, df);
+ this.currentFieldStoresPayloads = storesPayloads;
+ lastFreqPointer = freqBasePointer;
+ lastProxPointer = proxBasePointer;
+
+ Arrays.fill(freqPointer, freqBasePointer);
+ Arrays.fill(proxPointer, proxBasePointer);
+ Arrays.fill(payloadLength, 0);
+ }
+
+ /** Returns the freq pointer of the doc to which the last call of
+ * {@link MultiLevelSkipListReader#skipTo(int)} has skipped. */
+ long getFreqPointer() {
+ return lastFreqPointer;
+ }
+
+ /** Returns the prox pointer of the doc to which the last call of
+ * {@link MultiLevelSkipListReader#skipTo(int)} has skipped. */
+ long getProxPointer() {
+ return lastProxPointer;
+ }
+
+ /** Returns the payload length of the payload stored just before
+ * the doc to which the last call of {@link MultiLevelSkipListReader#skipTo(int)}
+ * has skipped. */
+ int getPayloadLength() {
+ return lastPayloadLength;
+ }
+
+ protected void seekChild(int level) throws IOException {
+ super.seekChild(level);
+ freqPointer[level] = lastFreqPointer;
+ proxPointer[level] = lastProxPointer;
+ payloadLength[level] = lastPayloadLength;
+ }
+
+ protected void setLastSkipData(int level) {
+ super.setLastSkipData(level);
+ lastFreqPointer = freqPointer[level];
+ lastProxPointer = proxPointer[level];
+ lastPayloadLength = payloadLength[level];
+ }
+
+
+ protected int readSkipData(int level, IndexInput skipStream) throws IOException {
+ int delta;
+ if (currentFieldStoresPayloads) {
+ // the current field stores payloads.
+ // if the doc delta is odd then we have
+ // to read the current payload length
+ // because it differs from the length of the
+ // previous payload
+ delta = skipStream.readVInt();
+ if ((delta & 1) != 0) {
+ payloadLength[level] = skipStream.readVInt();
+ }
+ delta >>>= 1;
+ } else {
+ delta = skipStream.readVInt();
+ }
+ freqPointer[level] += skipStream.readVInt();
+ proxPointer[level] += skipStream.readVInt();
+
+ return delta;
+ }
+}
diff --git a/src/java/org/apache/lucene/index/DefaultSkipListWriter.java b/src/java/org/apache/lucene/index/DefaultSkipListWriter.java
new file mode 100644
index 00000000000..124dd94c20e
--- /dev/null
+++ b/src/java/org/apache/lucene/index/DefaultSkipListWriter.java
@@ -0,0 +1,124 @@
+package org.apache.lucene.index;
+
+/**
+ * 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.
+ */
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.lucene.store.IndexOutput;
+
+
+/**
+ * Implements the skip list writer for the default posting list format
+ * that stores positions and payloads.
+ *
+ */
+class DefaultSkipListWriter extends MultiLevelSkipListWriter {
+ private int[] lastSkipDoc;
+ private int[] lastSkipPayloadLength;
+ private long[] lastSkipFreqPointer;
+ private long[] lastSkipProxPointer;
+
+ private IndexOutput freqOutput;
+ private IndexOutput proxOutput;
+
+ private int curDoc;
+ private boolean curStorePayloads;
+ private int curPayloadLength;
+ private long curFreqPointer;
+ private long curProxPointer;
+
+ DefaultSkipListWriter(int skipInterval, int numberOfSkipLevels, int docCount, IndexOutput freqOutput, IndexOutput proxOutput) {
+ super(skipInterval, numberOfSkipLevels, docCount);
+ this.freqOutput = freqOutput;
+ this.proxOutput = proxOutput;
+
+ lastSkipDoc = new int[numberOfSkipLevels];
+ lastSkipPayloadLength = new int[numberOfSkipLevels];
+ lastSkipFreqPointer = new long[numberOfSkipLevels];
+ lastSkipProxPointer = new long[numberOfSkipLevels];
+ }
+
+ /**
+ * Sets the values for the current skip data.
+ */
+ void setSkipData(int doc, boolean storePayloads, int payloadLength) {
+ this.curDoc = doc;
+ this.curStorePayloads = storePayloads;
+ this.curPayloadLength = payloadLength;
+ this.curFreqPointer = freqOutput.getFilePointer();
+ this.curProxPointer = proxOutput.getFilePointer();
+ }
+
+ protected void resetSkip() {
+ super.resetSkip();
+ Arrays.fill(lastSkipDoc, 0);
+ Arrays.fill(lastSkipPayloadLength, -1); // we don't have to write the first length in the skip list
+ Arrays.fill(lastSkipFreqPointer, freqOutput.getFilePointer());
+ Arrays.fill(lastSkipProxPointer, proxOutput.getFilePointer());
+ }
+
+ protected void writeSkipData(int level, IndexOutput skipBuffer) throws IOException {
+ // To efficiently store payloads in the posting lists we do not store the length of
+ // every payload. Instead we omit the length for a payload if the previous payload had
+ // the same length.
+ // However, in order to support skipping the payload length at every skip point must be known.
+ // So we use the same length encoding that we use for the posting lists for the skip data as well:
+ // Case 1: current field does not store payloads
+ // SkipDatum --> DocSkip, FreqSkip, ProxSkip
+ // DocSkip,FreqSkip,ProxSkip --> VInt
+ // DocSkip records the document number before every SkipInterval th document in TermFreqs.
+ // Document numbers are represented as differences from the previous value in the sequence.
+ // Case 2: current field stores payloads
+ // SkipDatum --> DocSkip, PayloadLength?, FreqSkip,ProxSkip
+ // DocSkip,FreqSkip,ProxSkip --> VInt
+ // PayloadLength --> VInt
+ // In this case DocSkip/2 is the difference between
+ // the current and the previous value. If DocSkip
+ // is odd, then a PayloadLength encoded as VInt follows,
+ // if DocSkip is even, then it is assumed that the
+ // current payload length equals the length at the previous
+ // skip point
+ if (curStorePayloads) {
+ int delta = curDoc - lastSkipDoc[level];
+ if (curPayloadLength == lastSkipPayloadLength[level]) {
+ // the current payload length equals the length at the previous skip point,
+ // so we don't store the length again
+ skipBuffer.writeVInt(delta * 2);
+ } else {
+ // the payload length is different from the previous one. We shift the DocSkip,
+ // set the lowest bit and store the current payload length as VInt.
+ skipBuffer.writeVInt(delta * 2 + 1);
+ skipBuffer.writeVInt(curPayloadLength);
+ lastSkipPayloadLength[level] = curPayloadLength;
+ }
+ } else {
+ // current field does not store payloads
+ skipBuffer.writeVInt(curDoc - lastSkipDoc[level]);
+ }
+ skipBuffer.writeVInt((int) (curFreqPointer - lastSkipFreqPointer[level]));
+ skipBuffer.writeVInt((int) (curProxPointer - lastSkipProxPointer[level]));
+
+ lastSkipDoc[level] = curDoc;
+ //System.out.println("write doc at level " + level + ": " + curDoc);
+
+ lastSkipFreqPointer[level] = curFreqPointer;
+ lastSkipProxPointer[level] = curProxPointer;
+ }
+
+}
diff --git a/src/java/org/apache/lucene/index/MultiLevelSkipListReader.java b/src/java/org/apache/lucene/index/MultiLevelSkipListReader.java
new file mode 100644
index 00000000000..73cb861ae9d
--- /dev/null
+++ b/src/java/org/apache/lucene/index/MultiLevelSkipListReader.java
@@ -0,0 +1,271 @@
+package org.apache.lucene.index;
+
+/**
+ * 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.
+ */
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.lucene.store.BufferedIndexInput;
+import org.apache.lucene.store.IndexInput;
+
+/**
+ * This abstract class reads skip lists with multiple levels.
+ *
+ * See {@link MultiLevelSkipListWriter} for the information about the encoding
+ * of the multi level skip lists.
+ *
+ * Subclasses must implement the abstract method {@link #readSkipData(int, IndexInput)}
+ * which defines the actual format of the skip data.
+ */
+abstract class MultiLevelSkipListReader {
+ // the maximum number of skip levels possible for this index
+ private int maxNumberOfSkipLevels;
+
+ // number of levels in this skip list
+ private int numberOfSkipLevels;
+
+ // Expert: defines the number of top skip levels to buffer in memory.
+ // Reducing this number results in less memory usage, but possibly
+ // slower performance due to more random I/Os.
+ // Please notice that the space each level occupies is limited by
+ // the skipInterval. The top level can not contain more than
+ // skipLevel entries, the second top level can not contain more
+ // than skipLevel^2 entries and so forth.
+ private int numberOfLevelsToBuffer = 1;
+
+ private int docCount;
+ private boolean haveSkipped;
+
+ private IndexInput[] skipStream; // skipStream for each level
+ private long skipPointer[]; // the start pointer of each skip level
+ private int skipInterval[]; // skipInterval of each level
+ private int[] numSkipped; // number of docs skipped per level
+
+ private int[] skipDoc; // doc id of current skip entry per level
+ private int lastDoc; // doc id of last read skip entry with docId <= target
+ private long[] childPointer; // child pointer of current skip entry per level
+ private long lastChildPointer; // childPointer of last read skip entry with docId <= target
+
+ private boolean inputIsBuffered;
+
+ public MultiLevelSkipListReader(IndexInput skipStream, int maxSkipLevels, int skipInterval) {
+ this.skipStream = new IndexInput[maxSkipLevels];
+ this.skipPointer = new long[maxSkipLevels];
+ this.childPointer = new long[maxSkipLevels];
+ this.numSkipped = new int[maxSkipLevels];
+ this.maxNumberOfSkipLevels = maxSkipLevels;
+ this.skipInterval = new int[maxSkipLevels];
+ this.skipStream [0]= skipStream;
+ this.inputIsBuffered = (skipStream instanceof BufferedIndexInput);
+ this.skipInterval[0] = skipInterval;
+ for (int i = 1; i < maxSkipLevels; i++) {
+ // cache skip intervals
+ this.skipInterval[i] = this.skipInterval[i - 1] * skipInterval;
+ }
+ skipDoc = new int[maxSkipLevels];
+ }
+
+
+ /** Returns the id of the doc to which the last call of {@link #skipTo(int)}
+ * has skipped. */
+ int getDoc() {
+ return lastDoc;
+ }
+
+
+ /** Skips entries to the first beyond the current whose document number is
+ * greater than or equal to target. Returns the current doc count.
+ */
+ int skipTo(int target) throws IOException {
+ if (!haveSkipped) {
+ // first time, load skip levels
+ loadSkipLevels();
+ haveSkipped = true;
+ }
+
+ // walk up the levels until highest level is found that has a skip
+ // for this target
+ int level = 0;
+ while (level < numberOfSkipLevels - 1 && target > skipDoc[level + 1]) {
+ level++;
+ }
+
+ while (level >= 0) {
+ if (target > skipDoc[level]) {
+ if (!loadNextSkip(level)) {
+ continue;
+ }
+ } else {
+ // no more skips on this level, go down one level
+ if (level > 0 && lastChildPointer > skipStream[level - 1].getFilePointer()) {
+ seekChild(level - 1);
+ }
+ level--;
+ }
+ }
+
+ return numSkipped[0] - skipInterval[0] - 1;
+ }
+
+ private boolean loadNextSkip(int level) throws IOException {
+ // we have to skip, the target document is greater than the current
+ // skip list entry
+ setLastSkipData(level);
+
+ numSkipped[level] += skipInterval[level];
+
+ if (numSkipped[level] > docCount) {
+ // this skip list is exhausted
+ skipDoc[level] = Integer.MAX_VALUE;
+ if (numberOfSkipLevels > level) numberOfSkipLevels = level;
+ return false;
+ }
+
+ // read next skip entry
+ skipDoc[level] += readSkipData(level, skipStream[level]);
+
+ if (level != 0) {
+ // read the child pointer if we are not on the leaf level
+ childPointer[level] = skipStream[level].readVLong() + skipPointer[level - 1];
+ }
+
+ return true;
+
+ }
+
+ /** Seeks the skip entry on the given level */
+ protected void seekChild(int level) throws IOException {
+ skipStream[level].seek(lastChildPointer);
+ numSkipped[level] = numSkipped[level + 1] - skipInterval[level + 1];
+ skipDoc[level] = lastDoc;
+ if (level > 0) {
+ childPointer[level] = skipStream[level].readVLong() + skipPointer[level - 1];
+ }
+ }
+
+ void close() throws IOException {
+ for (int i = 1; i < skipStream.length; i++) {
+ if (skipStream[i] != null) {
+ skipStream[i].close();
+ }
+ }
+ }
+
+ /** initializes the reader */
+ void init(long skipPointer, int df) {
+ this.skipPointer[0] = skipPointer;
+ this.docCount = df;
+ Arrays.fill(skipDoc, 0);
+ Arrays.fill(numSkipped, 0);
+ haveSkipped = false;
+ for (int i = 1; i < numberOfSkipLevels; i++) {
+ skipStream[0] = null;
+ }
+ }
+
+ /** Loads the skip levels */
+ private void loadSkipLevels() throws IOException {
+ numberOfSkipLevels = docCount == 0 ? 0 : (int) Math.floor(Math.log(docCount) / Math.log(skipInterval[0]));
+ if (numberOfSkipLevels > maxNumberOfSkipLevels) {
+ numberOfSkipLevels = maxNumberOfSkipLevels;
+ }
+
+ skipStream[0].seek(skipPointer[0]);
+
+ int toBuffer = numberOfLevelsToBuffer;
+
+ for (int i = numberOfSkipLevels - 1; i > 0; i--) {
+ // the length of the current level
+ long length = skipStream[0].readVLong();
+
+ // the start pointer of the current level
+ skipPointer[i] = skipStream[0].getFilePointer();
+ if (toBuffer > 0) {
+ // buffer this level
+ skipStream[i] = new SkipBuffer(skipStream[0], (int) length);
+ toBuffer--;
+ } else {
+ // clone this stream, it is already at the start of the current level
+ skipStream[i] = (IndexInput) skipStream[0].clone();
+ if (inputIsBuffered && length < BufferedIndexInput.BUFFER_SIZE) {
+ ((BufferedIndexInput) skipStream[i]).setBufferSize((int) length);
+ }
+
+ // move base stream beyond the current level
+ skipStream[0].seek(skipStream[0].getFilePointer() + length);
+ }
+ }
+
+ // use base stream for the lowest level
+ skipPointer[0] = skipStream[0].getFilePointer();
+ }
+
+ /**
+ * Subclasses must implement the actual skip data encoding in this method.
+ *
+ * @param level the level skip data shall be read from
+ * @param skipStream the skip stream to read from
+ */
+ protected abstract int readSkipData(int level, IndexInput skipStream) throws IOException;
+
+ /** Copies the values of the last read skip entry on this level */
+ protected void setLastSkipData(int level) {
+ lastDoc = skipDoc[level];
+ lastChildPointer = childPointer[level];
+ }
+
+
+ /** used to buffer the top skip levels */
+ private final static class SkipBuffer extends IndexInput {
+ private byte[] data;
+ private long pointer;
+ private int pos;
+
+ SkipBuffer(IndexInput input, int length) throws IOException {
+ data = new byte[length];
+ pointer = input.getFilePointer();
+ input.readBytes(data, 0, length);
+ }
+
+ public void close() throws IOException {
+ data = null;
+ }
+
+ public long getFilePointer() {
+ return pointer + pos;
+ }
+
+ public long length() {
+ return data.length;
+ }
+
+ public byte readByte() throws IOException {
+ return data[pos++];
+ }
+
+ public void readBytes(byte[] b, int offset, int len) throws IOException {
+ System.arraycopy(data, pos, b, offset, len);
+ pos += len;
+ }
+
+ public void seek(long pos) throws IOException {
+ this.pos = (int) (pos - pointer);
+ }
+
+ }
+}
diff --git a/src/java/org/apache/lucene/index/MultiLevelSkipListWriter.java b/src/java/org/apache/lucene/index/MultiLevelSkipListWriter.java
new file mode 100644
index 00000000000..3aa9b5efe0f
--- /dev/null
+++ b/src/java/org/apache/lucene/index/MultiLevelSkipListWriter.java
@@ -0,0 +1,151 @@
+package org.apache.lucene.index;
+
+/**
+ * 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.
+ */
+
+import java.io.IOException;
+
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.store.RAMOutputStream;
+
+/**
+ * This abstract class writes skip lists with multiple levels.
+ *
+ * Example for skipInterval = 3:
+ * c (skip level 2)
+ * c c c (skip level 1)
+ * x x x x x x x x x x (skip level 0)
+ * d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d d (posting list)
+ * 3 6 9 12 15 18 21 24 27 30 (df)
+ *
+ * d - document
+ * x - skip data
+ * c - skip data with child pointer
+ *
+ * Skip level i contains every skipInterval-th entry from skip level i-1.
+ * Therefore the number of entries on level i is: floor(df / ((skipInterval ^ (i + 1))).
+ *
+ * Each skip entry on a level i>0 contains a pointer to the corresponding skip entry in list i-1.
+ * This guarantess a logarithmic amount of skips to find the target document.
+ *
+ * While this class takes care of writing the different skip levels,
+ * subclasses must define the actual format of the skip data.
+ *
+ */
+abstract class MultiLevelSkipListWriter {
+ // number of levels in this skip list
+ private int numberOfSkipLevels;
+
+ // the skip interval in the list with level = 0
+ private int skipInterval;
+
+ // for every skip level a different buffer is used
+ private RAMOutputStream[] skipBuffer;
+
+ protected MultiLevelSkipListWriter(int skipInterval, int maxSkipLevels, int df) {
+ this.skipInterval = skipInterval;
+
+ // calculate the maximum number of skip levels for this document frequency
+ numberOfSkipLevels = df == 0 ? 0 : (int) Math.floor(Math.log(df) / Math.log(skipInterval));
+
+ // make sure it does not exceed maxSkipLevels
+ if (numberOfSkipLevels > maxSkipLevels) {
+ numberOfSkipLevels = maxSkipLevels;
+ }
+ }
+
+ protected void init() {
+ skipBuffer = new RAMOutputStream[numberOfSkipLevels];
+ for (int i = 0; i < numberOfSkipLevels; i++) {
+ skipBuffer[i] = new RAMOutputStream();
+ }
+ }
+
+ protected void resetSkip() {
+ // creates new buffers or empties the existing ones
+ if (skipBuffer == null) {
+ init();
+ } else {
+ for (int i = 0; i < skipBuffer.length; i++) {
+ skipBuffer[i].reset();
+ }
+ }
+ }
+
+ /**
+ * Subclasses must implement the actual skip data encoding in this method.
+ *
+ * @param level the level skip data shall be writting for
+ * @param skipBuffer the skip buffer to write to
+ */
+ protected abstract void writeSkipData(int level, IndexOutput skipBuffer) throws IOException;
+
+ /**
+ * Writes the current skip data to the buffers. The current document frequency determines
+ * the max level is skip data is to be written to.
+ *
+ * @param df the current document frequency
+ * @throws IOException
+ */
+ void bufferSkip(int df) throws IOException {
+ int numLevels;
+
+ // determine max level
+ for (numLevels = 0; (df % skipInterval) == 0 && numLevels < numberOfSkipLevels; df /= skipInterval) {
+ numLevels++;
+ }
+
+ long childPointer = 0;
+
+ for (int level = 0; level < numLevels; level++) {
+ writeSkipData(level, skipBuffer[level]);
+
+ long newChildPointer = skipBuffer[level].getFilePointer();
+
+ if (level != 0) {
+ // store child pointers for all levels except the lowest
+ skipBuffer[level].writeVLong(childPointer);
+ }
+
+ //remember the childPointer for the next level
+ childPointer = newChildPointer;
+ }
+ }
+
+ /**
+ * Writes the buffered skip lists to the given output.
+ *
+ * @param output the IndexOutput the skip lists shall be written to
+ * @return the pointer the skip list starts
+ */
+ long writeSkip(IndexOutput output) throws IOException {
+ long skipPointer = output.getFilePointer();
+ if (skipBuffer == null || skipBuffer.length == 0) return skipPointer;
+
+ for (int level = numberOfSkipLevels - 1; level > 0; level--) {
+ long length = skipBuffer[level].getFilePointer();
+ if (length > 0) {
+ output.writeVLong(length);
+ skipBuffer[level].writeTo(output);
+ }
+ }
+ skipBuffer[0].writeTo(output);
+
+ return skipPointer;
+ }
+
+}
diff --git a/src/java/org/apache/lucene/index/SegmentMerger.java b/src/java/org/apache/lucene/index/SegmentMerger.java
index 91d7dce2dcf..c0e84c0492b 100644
--- a/src/java/org/apache/lucene/index/SegmentMerger.java
+++ b/src/java/org/apache/lucene/index/SegmentMerger.java
@@ -26,7 +26,6 @@ import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.document.FieldSelectorResult;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexOutput;
-import org.apache.lucene.store.RAMOutputStream;
/**
* The SegmentMerger class combines two or more Segments, represented by an IndexReader ({@link #add},
@@ -50,6 +49,8 @@ final class SegmentMerger {
private Vector readers = new Vector();
private FieldInfos fieldInfos;
+
+ private int mergedDocs;
/** This ctor used only by test code.
*
@@ -93,14 +94,14 @@ final class SegmentMerger {
final int merge() throws CorruptIndexException, IOException {
int value;
- value = mergeFields();
+ mergedDocs = mergeFields();
mergeTerms();
mergeNorms();
if (fieldInfos.hasVectors())
mergeVectors();
- return value;
+ return mergedDocs;
}
/**
@@ -241,7 +242,9 @@ final class SegmentMerger {
private IndexOutput proxOutput = null;
private TermInfosWriter termInfosWriter = null;
private int skipInterval;
+ private int maxSkipLevels;
private SegmentMergeQueue queue = null;
+ private DefaultSkipListWriter skipListWriter = null;
private final void mergeTerms() throws CorruptIndexException, IOException {
try {
@@ -251,6 +254,8 @@ final class SegmentMerger {
new TermInfosWriter(directory, segment, fieldInfos,
termIndexInterval);
skipInterval = termInfosWriter.skipInterval;
+ maxSkipLevels = termInfosWriter.maxSkipLevels;
+ skipListWriter = new DefaultSkipListWriter(skipInterval, maxSkipLevels, mergedDocs, freqOutput, proxOutput);
queue = new SegmentMergeQueue(readers.size());
mergeTermInfos();
@@ -319,7 +324,7 @@ final class SegmentMerger {
int df = appendPostings(smis, n); // append posting data
- long skipPointer = writeSkip();
+ long skipPointer = skipListWriter.writeSkip(freqOutput);
if (df > 0) {
// add an entry to the dictionary with pointers to prox and freq files
@@ -344,7 +349,7 @@ final class SegmentMerger {
throws CorruptIndexException, IOException {
int lastDoc = 0;
int df = 0; // number of docs w/ term
- resetSkip();
+ skipListWriter.resetSkip();
boolean storePayloads = fieldInfos.fieldInfo(smis[0].term.field).storePayloads;
int lastPayloadLength = -1; // ensures that we write the first length
for (int i = 0; i < n; i++) {
@@ -366,7 +371,8 @@ final class SegmentMerger {
df++;
if ((df % skipInterval) == 0) {
- bufferSkip(lastDoc, storePayloads, lastPayloadLength);
+ skipListWriter.setSkipData(lastDoc, storePayloads, lastPayloadLength);
+ skipListWriter.bufferSkip(df);
}
int docCode = (doc - lastDoc) << 1; // use low bit to flag freq=1
@@ -413,75 +419,6 @@ final class SegmentMerger {
return df;
}
- private RAMOutputStream skipBuffer = new RAMOutputStream();
- private int lastSkipDoc;
- private int lastSkipPayloadLength;
- private long lastSkipFreqPointer;
- private long lastSkipProxPointer;
-
- private void resetSkip() {
- skipBuffer.reset();
- lastSkipDoc = 0;
- lastSkipPayloadLength = -1; // we don't have to write the first length in the skip list
- lastSkipFreqPointer = freqOutput.getFilePointer();
- lastSkipProxPointer = proxOutput.getFilePointer();
- }
-
- private void bufferSkip(int doc, boolean storePayloads, int payloadLength) throws IOException {
- long freqPointer = freqOutput.getFilePointer();
- long proxPointer = proxOutput.getFilePointer();
-
- // To efficiently store payloads in the posting lists we do not store the length of
- // every payload. Instead we omit the length for a payload if the previous payload had
- // the same length.
- // However, in order to support skipping the payload length at every skip point must be known.
- // So we use the same length encoding that we use for the posting lists for the skip data as well:
- // Case 1: current field does not store payloads
- // SkipDatum --> DocSkip, FreqSkip, ProxSkip
- // DocSkip,FreqSkip,ProxSkip --> VInt
- // DocSkip records the document number before every SkipInterval th document in TermFreqs.
- // Document numbers are represented as differences from the previous value in the sequence.
- // Case 2: current field stores payloads
- // SkipDatum --> DocSkip, PayloadLength?, FreqSkip,ProxSkip
- // DocSkip,FreqSkip,ProxSkip --> VInt
- // PayloadLength --> VInt
- // In this case DocSkip/2 is the difference between
- // the current and the previous value. If DocSkip
- // is odd, then a PayloadLength encoded as VInt follows,
- // if DocSkip is even, then it is assumed that the
- // current payload length equals the length at the previous
- // skip point
- if (storePayloads) {
- int delta = doc - lastSkipDoc;
- if (payloadLength == lastSkipPayloadLength) {
- // the current payload length equals the length at the previous skip point,
- // so we don't store the length again
- skipBuffer.writeVInt(delta * 2);
- } else {
- // the payload length is different from the previous one. We shift the DocSkip,
- // set the lowest bit and store the current payload length as VInt.
- skipBuffer.writeVInt(delta * 2 + 1);
- skipBuffer.writeVInt(payloadLength);
- lastSkipPayloadLength = payloadLength;
- }
- } else {
- // current field does not store payloads
- skipBuffer.writeVInt(doc - lastSkipDoc);
- }
- skipBuffer.writeVInt((int) (freqPointer - lastSkipFreqPointer));
- skipBuffer.writeVInt((int) (proxPointer - lastSkipProxPointer));
-
- lastSkipDoc = doc;
- lastSkipFreqPointer = freqPointer;
- lastSkipProxPointer = proxPointer;
- }
-
- private long writeSkip() throws IOException {
- long skipPointer = freqOutput.getFilePointer();
- skipBuffer.writeTo(freqOutput);
- return skipPointer;
- }
-
private void mergeNorms() throws IOException {
byte[] normBuffer = null;
IndexOutput output = null;
diff --git a/src/java/org/apache/lucene/index/SegmentTermDocs.java b/src/java/org/apache/lucene/index/SegmentTermDocs.java
index 253b3580f7a..4c429d16281 100644
--- a/src/java/org/apache/lucene/index/SegmentTermDocs.java
+++ b/src/java/org/apache/lucene/index/SegmentTermDocs.java
@@ -31,16 +31,15 @@ class SegmentTermDocs implements TermDocs {
int freq;
private int skipInterval;
- private int numSkips;
- private int skipCount;
- private IndexInput skipStream;
- private int skipDoc;
- private long freqPointer;
- private long proxPointer;
+ private int maxSkipLevels;
+ private DefaultSkipListReader skipListReader;
+
+ private long freqBasePointer;
+ private long proxBasePointer;
+
private long skipPointer;
private boolean haveSkipped;
- private int payloadLengthAtLastSkip;
protected boolean currentFieldStoresPayloads;
protected SegmentTermDocs(SegmentReader parent) {
@@ -48,6 +47,7 @@ class SegmentTermDocs implements TermDocs {
this.freqStream = (IndexInput) parent.freqStream.clone();
this.deletedDocs = parent.deletedDocs;
this.skipInterval = parent.tis.getSkipInterval();
+ this.maxSkipLevels = parent.tis.getMaxSkipLevels();
}
public void seek(Term term) throws IOException {
@@ -74,7 +74,6 @@ class SegmentTermDocs implements TermDocs {
void seek(TermInfo ti, Term term) throws IOException {
count = 0;
- payloadLengthAtLastSkip = 0;
FieldInfo fi = parent.fieldInfos.fieldInfo(term.field);
currentFieldStoresPayloads = (fi != null) ? fi.storePayloads : false;
if (ti == null) {
@@ -82,21 +81,18 @@ class SegmentTermDocs implements TermDocs {
} else {
df = ti.docFreq;
doc = 0;
- skipDoc = 0;
- skipCount = 0;
- numSkips = df / skipInterval;
- freqPointer = ti.freqPointer;
- proxPointer = ti.proxPointer;
- skipPointer = freqPointer + ti.skipOffset;
- freqStream.seek(freqPointer);
+ freqBasePointer = ti.freqPointer;
+ proxBasePointer = ti.proxPointer;
+ skipPointer = freqBasePointer + ti.skipOffset;
+ freqStream.seek(freqBasePointer);
haveSkipped = false;
}
}
public void close() throws IOException {
freqStream.close();
- if (skipStream != null)
- skipStream.close();
+ if (skipListReader != null)
+ skipListReader.close();
}
public final int doc() { return doc; }
@@ -111,11 +107,11 @@ class SegmentTermDocs implements TermDocs {
return false;
int docCode = freqStream.readVInt();
- doc += docCode >>> 1; // shift off low bit
- if ((docCode & 1) != 0) // if low bit is set
- freq = 1; // freq is one
+ doc += docCode >>> 1; // shift off low bit
+ if ((docCode & 1) != 0) // if low bit is set
+ freq = 1; // freq is one
else
- freq = freqStream.readVInt(); // else read freq
+ freq = freqStream.readVInt(); // else read freq
count++;
@@ -135,11 +131,11 @@ class SegmentTermDocs implements TermDocs {
// manually inlined call to next() for speed
final int docCode = freqStream.readVInt();
- doc += docCode >>> 1; // shift off low bit
- if ((docCode & 1) != 0) // if low bit is set
- freq = 1; // freq is one
+ doc += docCode >>> 1; // shift off low bit
+ if ((docCode & 1) != 0) // if low bit is set
+ freq = 1; // freq is one
else
- freq = freqStream.readVInt(); // else read freq
+ freq = freqStream.readVInt(); // else read freq
count++;
if (deletedDocs == null || !deletedDocs.get(doc)) {
@@ -157,64 +153,22 @@ class SegmentTermDocs implements TermDocs {
/** Optimized implementation. */
public boolean skipTo(int target) throws IOException {
if (df >= skipInterval) { // optimized case
+ if (skipListReader == null)
+ skipListReader = new DefaultSkipListReader((IndexInput) freqStream.clone(), maxSkipLevels, skipInterval); // lazily clone
- if (skipStream == null)
- skipStream = (IndexInput) freqStream.clone(); // lazily clone
-
- if (!haveSkipped) { // lazily seek skip stream
- skipStream.seek(skipPointer);
+ if (!haveSkipped) { // lazily initialize skip stream
+ skipListReader.init(skipPointer, freqBasePointer, proxBasePointer, df, currentFieldStoresPayloads);
haveSkipped = true;
}
- // scan skip data
- int lastSkipDoc = skipDoc;
- int lastPayloadLength = 0;
- long lastFreqPointer = freqStream.getFilePointer();
- long lastProxPointer = -1;
- int numSkipped = -1 - (count % skipInterval);
-
- while (target > skipDoc) {
- lastSkipDoc = skipDoc;
- lastFreqPointer = freqPointer;
- lastProxPointer = proxPointer;
- lastPayloadLength = payloadLengthAtLastSkip;
-
- if (skipDoc != 0 && skipDoc >= doc)
- numSkipped += skipInterval;
-
- if(skipCount >= numSkips)
- break;
-
- if (currentFieldStoresPayloads) {
- // the current field stores payloads.
- // if the doc delta is odd then we have
- // to read the current payload length
- // because it differs from the length of the
- // previous payload
- int delta = skipStream.readVInt();
- if ((delta & 1) != 0) {
- payloadLengthAtLastSkip = skipStream.readVInt();
- }
- delta >>>= 1;
- skipDoc += delta;
- } else {
- skipDoc += skipStream.readVInt();
- }
- freqPointer += skipStream.readVInt();
- proxPointer += skipStream.readVInt();
-
- skipCount++;
- }
-
- // if we found something to skip, then skip it
- if (lastFreqPointer > freqStream.getFilePointer()) {
- freqStream.seek(lastFreqPointer);
- skipProx(lastProxPointer, lastPayloadLength);
-
- doc = lastSkipDoc;
- count += numSkipped;
- }
+ int newCount = skipListReader.skipTo(target);
+ if (newCount > count) {
+ freqStream.seek(skipListReader.getFreqPointer());
+ skipProx(skipListReader.getProxPointer(), skipListReader.getPayloadLength());
+ doc = skipListReader.getDoc();
+ count = newCount;
+ }
}
// done skipping, now just scan
@@ -224,5 +178,4 @@ class SegmentTermDocs implements TermDocs {
} while (target > doc);
return true;
}
-
}
diff --git a/src/java/org/apache/lucene/index/SegmentTermEnum.java b/src/java/org/apache/lucene/index/SegmentTermEnum.java
index 52bded8deaf..8785019e254 100644
--- a/src/java/org/apache/lucene/index/SegmentTermEnum.java
+++ b/src/java/org/apache/lucene/index/SegmentTermEnum.java
@@ -37,6 +37,7 @@ final class SegmentTermEnum extends TermEnum implements Cloneable {
long indexPointer = 0;
int indexInterval;
int skipInterval;
+ int maxSkipLevels;
private int formatM1SkipInterval;
SegmentTermEnum(IndexInput i, FieldInfos fis, boolean isi)
@@ -44,7 +45,8 @@ final class SegmentTermEnum extends TermEnum implements Cloneable {
input = i;
fieldInfos = fis;
isIndex = isi;
-
+ maxSkipLevels = 1; // use single-level skip lists for formats > -3
+
int firstInt = input.readInt();
if (firstInt >= 0) {
// original-format file, without explicit format version number
@@ -54,7 +56,6 @@ final class SegmentTermEnum extends TermEnum implements Cloneable {
// back-compatible settings
indexInterval = 128;
skipInterval = Integer.MAX_VALUE; // switch off skipTo optimization
-
} else {
// we have a format version number
format = firstInt;
@@ -73,10 +74,13 @@ final class SegmentTermEnum extends TermEnum implements Cloneable {
// switch off skipTo optimization for file format prior to 1.4rc2 in order to avoid a bug in
// skipTo implementation of these versions
skipInterval = Integer.MAX_VALUE;
- }
- else{
+ } else {
indexInterval = input.readInt();
skipInterval = input.readInt();
+ if (format == -3) {
+ // this new format introduces multi-level skipping
+ maxSkipLevels = input.readInt();
+ }
}
}
diff --git a/src/java/org/apache/lucene/index/TermInfosReader.java b/src/java/org/apache/lucene/index/TermInfosReader.java
index 4e61978dc3b..89e2d2ef914 100644
--- a/src/java/org/apache/lucene/index/TermInfosReader.java
+++ b/src/java/org/apache/lucene/index/TermInfosReader.java
@@ -64,7 +64,11 @@ final class TermInfosReader {
public int getSkipInterval() {
return origEnum.skipInterval;
}
-
+
+ public int getMaxSkipLevels() {
+ return origEnum.maxSkipLevels;
+ }
+
final void close() throws IOException {
if (origEnum != null)
origEnum.close();
diff --git a/src/java/org/apache/lucene/index/TermInfosWriter.java b/src/java/org/apache/lucene/index/TermInfosWriter.java
index 17733c48e46..ce989151b39 100644
--- a/src/java/org/apache/lucene/index/TermInfosWriter.java
+++ b/src/java/org/apache/lucene/index/TermInfosWriter.java
@@ -28,7 +28,7 @@ import org.apache.lucene.util.StringHelper;
final class TermInfosWriter {
/** The file format version, a negative number. */
- public static final int FORMAT = -2;
+ public static final int FORMAT = -3;
private FieldInfos fieldInfos;
private IndexOutput output;
@@ -56,6 +56,11 @@ final class TermInfosWriter {
* smaller values result in bigger indexes, less acceleration and more
* accelerable cases. More detailed experiments would be useful here. */
int skipInterval = 16;
+
+ /** Expert: The maximum number of skip levels. Smaller values result in
+ * slightly smaller indexes, but slower skipping in big posting lists.
+ */
+ int maxSkipLevels = 10;
private long lastIndexPointer = 0;
private boolean isIndex = false;
@@ -85,6 +90,7 @@ final class TermInfosWriter {
output.writeLong(0); // leave space for size
output.writeInt(indexInterval); // write indexInterval
output.writeInt(skipInterval); // write skipInterval
+ output.writeInt(maxSkipLevels); // write maxSkipLevels
}
/** Adds a new pair to the set.
diff --git a/src/site/src/documentation/content/xdocs/fileformats.xml b/src/site/src/documentation/content/xdocs/fileformats.xml
index 47e368d9a6d..60a8bd98298 100644
--- a/src/site/src/documentation/content/xdocs/fileformats.xml
+++ b/src/site/src/documentation/content/xdocs/fileformats.xml
@@ -1139,7 +1139,7 @@
TermInfoFile (.tis)-->
- TIVersion, TermCount, IndexInterval, SkipInterval, TermInfos
+ TIVersion, TermCount, IndexInterval, SkipInterval, MaxSkipLevels, TermInfos
TIVersion -->
UInt32
@@ -1153,6 +1153,9 @@
SkipInterval -->
UInt32
+ MaxSkipLevels -->
+ UInt32
+
TermInfos -->
<TermInfo>
TermCount
@@ -1209,7 +1212,8 @@
particular, it is the number of bytes
after TermFreqs that the SkipData starts.
In other words, it is the length of the
- TermFreq data.
+ TermFreq data. SkipDelta is only stored
+ if DocFreq is not smaller than SkipInterval.
@@ -1233,7 +1237,7 @@
TermInfoIndex (.tii)-->
- TIVersion, IndexTermCount, IndexInterval, SkipInterval, TermIndices
+ TIVersion, IndexTermCount, IndexInterval, SkipInterval, MaxSkipLevels, TermIndices
TIVersion -->
UInt32
@@ -1261,8 +1265,11 @@
SkipInterval is the fraction of TermDocs stored in skip tables. It is used to accelerate TermDocs.skipTo(int).
Larger values result in smaller indexes, greater acceleration, but fewer accelerable cases, while
- smaller values result in bigger indexes, less acceleration and more
+ smaller values result in bigger indexes, less acceleration (in case of a small value for MaxSkipLevels) and more
accelerable cases.
+ MaxSkipLevels is the max. number of skip levels stored for each term in the .frq file. A low value results in
+ smaller indexes but less acceleration, a larger value results in slighly larger indexes but greater acceleration.
+ See format of .frq file for more information about skip levels.
@@ -1286,15 +1293,23 @@
DocDelta, Freq?
SkipData -->
+ <<SkipLevelLength, SkipLevel>
+ NumSkipLevels-1, SkipLevel>
<SkipDatum>
- DocFreq/SkipInterval
+
+ SkipLevel -->
+ <SkipDatum>
+ DocFreq/(SkipInterval^(Level + 1))
SkipDatum -->
- DocSkip,PayloadLength?,FreqSkip,ProxSkip
+ DocSkip,PayloadLength?,FreqSkip,ProxSkip,SkipChildLevelPointer?
DocDelta,Freq,DocSkip,PayloadLength,FreqSkip,ProxSkip -->
VInt
+ SkipChildLevelPointer -->
+ VLong
+
TermFreqs
are ordered by term (the term is implicit, from the .tis file).
@@ -1362,6 +1377,18 @@
nd
starts.
+ Lucene 2.2 introduces the notion of skip levels. Each term can have multiple skip levels.
+ The amount of skip levels for a term is NumSkipLevels = Min(MaxSkipLevels, floor(log(DocFreq/log(SkipInterval)))).
+ The number of SkipData entries for a skip level is DocFreq/(SkipInterval^(Level + 1)), whereas the lowest skip
+ level is Level=0.
+ Example: SkipInterval = 4, MaxSkipLevels = 2, DocFreq = 35. Then skip level 0 has 8 SkipData entries,
+ containing the 3rd, 7th, 11th, 15th, 19th, 23rd,
+ 27th, and 31st document numbers in TermFreqs. Skip level 1 has 2 SkipData entries, containing the
+ 15th and 31st document numbers in TermFreqs.
+ The SkipData entries on all upper levels > 0 contain a SkipChildLevelPointer referencing the corresponding SkipData
+ entry in level-1. In the example has entry 15 on level 1 a pointer to entry 15 on level 0 and entry 31 on level 1 a pointer
+ to entry 31 on level 0.
+
Positions
diff --git a/src/test/org/apache/lucene/index/TestMultiLevelSkipList.java b/src/test/org/apache/lucene/index/TestMultiLevelSkipList.java
new file mode 100644
index 00000000000..7511ff14644
--- /dev/null
+++ b/src/test/org/apache/lucene/index/TestMultiLevelSkipList.java
@@ -0,0 +1,154 @@
+package org.apache.lucene.index;
+
+/**
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.Reader;
+
+import junit.framework.TestCase;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.LowerCaseTokenizer;
+import org.apache.lucene.analysis.Token;
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.Field.Index;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.store.RAMDirectory;
+
+/**
+ * This testcase tests whether multi-level skipping is being used
+ * to reduce I/O while skipping through posting lists.
+ *
+ * Skipping in general is already covered by several other
+ * testcases.
+ *
+ */
+public class TestMultiLevelSkipList extends TestCase {
+ public void testSimpleSkip() throws IOException {
+ RAMDirectory dir = new RAMDirectory();
+ IndexWriter writer = new IndexWriter(dir, new PayloadAnalyzer(), true);
+ Term term = new Term("test", "a");
+ for (int i = 0; i < 5000; i++) {
+ Document d1 = new Document();
+ d1.add(new Field(term.field(), term.text(), Store.NO, Index.TOKENIZED));
+ writer.addDocument(d1);
+ }
+ writer.flush();
+ writer.optimize();
+ writer.close();
+
+ IndexReader reader = IndexReader.open(dir);
+ SegmentTermPositions tp = (SegmentTermPositions) reader.termPositions(term);
+ tp.freqStream = new CountingStream(tp.freqStream);
+
+ tp.next();
+
+ checkSkipTo(tp, 14, 185); // no skips
+ checkSkipTo(tp, 17, 190); // one skip on level 0
+ checkSkipTo(tp, 287, 200); // one skip on level 1, two on level 0
+
+ // this test would fail if we had only one skip level,
+ // because than more bytes would be read from the freqStream
+ checkSkipTo(tp, 4800, 250);// one skip on level 2
+ }
+
+ public void checkSkipTo(TermPositions tp, int target, int maxCounter) throws IOException {
+ tp.skipTo(target);
+ if (maxCounter < counter) {
+ fail("Too many bytes read: " + counter);
+ }
+
+ assertEquals("Wrong document " + tp.doc() + " after skipTo target " + target, target, tp.doc());
+ assertEquals("Frequency is not 1: " + tp.freq(), 1,tp.freq());
+ tp.nextPosition();
+ byte[] b = new byte[1];
+ tp.getPayload(b, 0);
+ assertEquals("Wrong payload for the target " + target + ": " + b[0], (byte) target, b[0]);
+ }
+
+ private static class PayloadAnalyzer extends Analyzer {
+ public TokenStream tokenStream(String fieldName, Reader reader) {
+ return new PayloadFilter(new LowerCaseTokenizer(reader));
+ }
+
+ }
+
+ private static class PayloadFilter extends TokenFilter {
+ static int count = 0;
+
+ protected PayloadFilter(TokenStream input) {
+ super(input);
+ }
+
+ public Token next() throws IOException {
+ Token t = input.next();
+ if (t != null) {
+ t.setPayload(new Payload(new byte[] { (byte) count++ }));
+ }
+ return t;
+ }
+
+ }
+
+ private int counter = 0;
+
+ // Simply extends IndexInput in a way that we are able to count the number
+ // of bytes read
+ class CountingStream extends IndexInput {
+ private IndexInput input;
+
+ CountingStream(IndexInput input) {
+ this.input = input;
+ }
+
+ public byte readByte() throws IOException {
+ TestMultiLevelSkipList.this.counter++;
+ return this.input.readByte();
+ }
+
+ public void readBytes(byte[] b, int offset, int len) throws IOException {
+ TestMultiLevelSkipList.this.counter += len;
+ this.input.readBytes(b, offset, len);
+ }
+
+ public void close() throws IOException {
+ this.input.close();
+ }
+
+ public long getFilePointer() {
+ return this.input.getFilePointer();
+ }
+
+ public void seek(long pos) throws IOException {
+ this.input.seek(pos);
+ }
+
+ public long length() {
+ return this.input.length();
+ }
+
+ public Object clone() {
+ return new CountingStream((IndexInput) this.input.clone());
+ }
+
+ }
+}