Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lucene/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ API Changes

New Features
---------------------
* GITHUB#16418: Incremental doc-values updates. A set-only doc-values update (NUMERIC or BINARY) is written as a sparse
"delta" generation holding just the updated documents and overlaid on the base column at read time, instead of
rewriting the whole column, turning per-update write amplification from O(column) into O(updated docs). Enabled by
default; opt out with IndexWriterConfig#setIncrementalDocValuesUpdates(false). (Jim Ferenczi)

* GITHUB#16241: Add HangulCompositionCharFilter to analysis-nori for opt-in composition of
modern Hangul conjoining jamo before KoreanTokenizer. (Mingi Jeong)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ boolean hasValue() {
protected final int maxDoc;
protected PagedMutable docs;
protected int size;
// true once any doc's value was reset (removed); such a buffer cannot take the sparse incremental
// path. Protected so
// subclasses that override reset() (e.g. NumericDocValuesFieldUpdates) can set it too.
protected boolean anyReset;

protected DocValuesFieldUpdates(int maxDoc, long delGen, String field, DocValuesType type) {
this.maxDoc = maxDoc;
Expand Down Expand Up @@ -349,9 +353,15 @@ final synchronized int size() {
* @param doc the doc to update
*/
synchronized void reset(int doc) {
anyReset = true;
addInternal(doc, HAS_NO_VALUE_MASK);
}

/** Whether this buffer removed any doc's value (vs. only setting values). */
final synchronized boolean anyReset() {
return anyReset;
}

final synchronized int add(int doc) {
return addInternal(doc, HAS_VALUE_MASK);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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.index;

import java.io.IOException;
import org.apache.lucene.search.DocIdSetIterator;

/**
* Merges the doc iterators of several doc-values layers (ordered newest first) into the union of
* their docs and tracks which layer supplies the current value: the newest layer positioned on the
* current doc. Backed by a binary min-heap keyed by (docID, layer index), so advancing only
* re-positions the layers sitting on the current doc (the rest keep their place) instead of
* scanning every layer per doc. Shared by {@link OverlayNumericDocValues} and {@link
* OverlayBinaryDocValues}; layers are advanced with {@code advance} only (never {@code
* advanceExact}) so iteration and random access can be interleaved on the same instance.
*/
final class DocValuesOverlayMerger {

private final DocValuesIterator[] layers; // newest first
private final int[] heap; // 1-based; holds layer indices, ordered by (docID, index)
private final int size;
private int docID = -1;
private int winner = -1;

DocValuesOverlayMerger(DocValuesIterator[] layers) {
this.layers = layers;
this.size = layers.length;
this.heap = new int[size + 1];
// all layers start at docID -1, so indices in natural order are already a valid min-heap
for (int i = 0; i < size; i++) {
heap[i + 1] = i;
}
}

int docID() {
return docID;
}

/** Index of the layer supplying the current value, or -1 if the current doc has no value. */
int valueLayer() {
return winner;
}

int nextDoc() throws IOException {
return advance(docID + 1);
}

int advance(int target) throws IOException {
while (layers[heap[1]].docID() < target) {
layers[heap[1]].advance(target);
siftDown();
}
docID = layers[heap[1]].docID();
winner = docID == DocIdSetIterator.NO_MORE_DOCS ? -1 : heap[1];
return docID;
}

boolean advanceExact(int target) throws IOException {
while (layers[heap[1]].docID() < target) {
layers[heap[1]].advance(target);
siftDown();
}
docID = target;
winner = layers[heap[1]].docID() == target ? heap[1] : -1;
return winner != -1;
}

long cost() {
// Over-estimates the union (a doc in several layers counts once per layer); cost() is only an
// upper-bound hint.
long cost = 0;
for (DocValuesIterator layer : layers) {
cost += layer.cost();
}
return cost;
}

/** Restore the heap after the root layer advanced (its docID only ever increases). */
private void siftDown() {
int i = 1;
int node = heap[1];
while (true) {
int left = i << 1;
if (left > size) {
break;
}
int right = left + 1;
int child = right <= size && less(heap[right], heap[left]) ? right : left;
if (less(heap[child], node) == false) {
break;
}
heap[i] = heap[child];
i = child;
}
heap[i] = node;
}

private boolean less(int a, int b) {
int da = layers[a].docID();
int db = layers[b].docID();
if (da != db) {
return da < db;
}
return a < b; // same doc: the newer layer (smaller index) wins
}
}
10 changes: 9 additions & 1 deletion lucene/core/src/java/org/apache/lucene/index/IndexWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,8 @@ public IndexWriter(Directory d, IndexWriterConfig conf) throws IOException {
bufferedUpdatesStream::getCompletedDelGen,
infoStream,
conf.getSoftDeletesField(),
reader);
reader,
config);
if (config.getReaderPooling()) {
readerPool.enableReaderPooling();
}
Expand Down Expand Up @@ -3624,6 +3625,13 @@ private SegmentCommitInfo copySegmentAsIs(
newInfo.setFiles(info.info.files());
newInfoPerCommit.setFieldInfosFiles(info.getFieldInfosFiles());
newInfoPerCommit.setDocValuesUpdatesFiles(info.getDocValuesUpdatesFiles());
// Carry over the incremental doc-values overlay generations (the copied update files keep their
// gen numbers).
for (Map.Entry<Integer, long[]> e : info.getDocValuesOverlays().entrySet()) {
final long[] packed = e.getValue();
newInfoPerCommit.setDocValuesOverlay(
e.getKey(), packed[0], ArrayUtil.copyOfSubArray(packed, 1, packed.length));
}

Set<String> copiedFiles = new HashSet<>();
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ public enum OpenMode {
*/
public static final long DEFAULT_MAX_FULL_FLUSH_MERGE_WAIT_MILLIS = 500;

/**
* Default value for whether incremental doc-values updates are enabled: {@code true}. Enabled by
* default on this major version (an old reader refuses a segment carrying the overlay); minor
* backports ship it opt-in (disabled).
*/
public static final boolean DEFAULT_INCREMENTAL_DOC_VALUES_UPDATES = true;

/**
* Default number of doc-values delta generations kept before they are compacted: {@code 16}.
* Higher values lower write amplification but keep more generations (hence more files) live to
* overlay at read time.
*/
public static final int DEFAULT_MAX_DOC_VALUES_DELTA_GENERATIONS = 16;

// indicates whether this config instance is already attached to a writer.
// not final so that it can be cloned properly.
private SetOnce<IndexWriter> writer = new SetOnce<>();
Expand Down Expand Up @@ -324,6 +338,43 @@ public boolean getReaderPooling() {
return readerPooling;
}

/**
* When enabled, a doc-values update that only sets values (no removals) is written as a sparse
* "delta" generation holding just the updated documents and overlaid on the existing column at
* read time, rather than rewriting the whole column. This trades some read cost for much lower
* write amplification on frequently updated fields. Enabled by default; pass {@code false} to
* keep the classic full-column rewrite.
*
* <p>A commit whose segments carry sparse delta generations is written at a bumped segments-file
* version, so a Lucene version that predates this feature refuses to open it rather than
* misreading a delta as the whole column. A segment merge (including a force-merge) flattens the
* overlay back to a plain column, so once all overlays are merged away older versions can read
* the index again. Only takes effect when IndexWriter is first created.
*
* @lucene.experimental
*/
public IndexWriterConfig setIncrementalDocValuesUpdates(boolean incrementalDocValuesUpdates) {
this.incrementalDocValuesUpdates = incrementalDocValuesUpdates;
return this;
}

/**
* Number of sparse doc-values delta generations a field may accumulate before they are folded
* into a single sparse generation. Higher values reduce write amplification further but keep more
* generations (hence more live files, and a deeper read-time overlay) between folds. Only
* relevant when {@link #setIncrementalDocValuesUpdates(boolean)} is enabled.
*
* @lucene.experimental
*/
public IndexWriterConfig setMaxDocValuesDeltaGenerations(int maxDocValuesDeltaGenerations) {
if (maxDocValuesDeltaGenerations < 1) {
throw new IllegalArgumentException(
"maxDocValuesDeltaGenerations must be >= 1; got " + maxDocValuesDeltaGenerations);
}
this.maxDocValuesDeltaGenerations = maxDocValuesDeltaGenerations;
return this;
}

/**
* Expert: Controls when segments are flushed to disk during indexing. The {@link FlushPolicy}
* initialized during {@link IndexWriter} instantiation and once initialized the given instance is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public class LiveIndexWriterConfig {
/** True if calls to {@link IndexWriter#close()} should first do a commit. */
protected boolean commitOnClose = IndexWriterConfig.DEFAULT_COMMIT_ON_CLOSE;

/** True if set-only doc-values updates are written as sparse delta generations. */
protected volatile boolean incrementalDocValuesUpdates;

/** Number of doc-values delta generations kept before they are compacted. */
protected volatile int maxDocValuesDeltaGenerations;

/** The sort order to use to write merged segments. */
protected Sort indexSort = null;

Expand Down Expand Up @@ -137,6 +143,8 @@ public class LiveIndexWriterConfig {
mergePolicy = new TieredMergePolicy();
flushPolicy = new FlushByRamOrCountsPolicy();
readerPooling = IndexWriterConfig.DEFAULT_READER_POOLING;
incrementalDocValuesUpdates = IndexWriterConfig.DEFAULT_INCREMENTAL_DOC_VALUES_UPDATES;
maxDocValuesDeltaGenerations = IndexWriterConfig.DEFAULT_MAX_DOC_VALUES_DELTA_GENERATIONS;
perThreadHardLimitMB = IndexWriterConfig.DEFAULT_RAM_PER_THREAD_HARD_LIMIT_MB;
maxFullFlushMergeWaitMillis = IndexWriterConfig.DEFAULT_MAX_FULL_FLUSH_MERGE_WAIT_MILLIS;
eventListener = IndexWriterEventListener.NO_OP_LISTENER;
Expand Down Expand Up @@ -385,6 +393,24 @@ public boolean getUseCompoundFile() {
return useCompoundFile;
}

/**
* Returns {@code true} if set-only doc-values updates are written as sparse delta generations.
*
* @lucene.experimental
*/
public boolean getIncrementalDocValuesUpdates() {
return incrementalDocValuesUpdates;
}

/**
* Returns the number of doc-values delta generations kept before they are compacted.
*
* @lucene.experimental
*/
public int getMaxDocValuesDeltaGenerations() {
return maxDocValuesDeltaGenerations;
}

/**
* Returns <code>true</code> if {@link IndexWriter#close()} should first commit before closing.
*/
Expand Down Expand Up @@ -487,6 +513,10 @@ public String toString() {
sb.append("readerPooling=").append(getReaderPooling()).append("\n");
sb.append("perThreadHardLimitMB=").append(getRAMPerThreadHardLimitMB()).append("\n");
sb.append("useCompoundFile=").append(getUseCompoundFile()).append("\n");
sb.append("incrementalDocValuesUpdates=").append(getIncrementalDocValuesUpdates()).append("\n");
sb.append("maxDocValuesDeltaGenerations=")
.append(getMaxDocValuesDeltaGenerations())
.append("\n");
sb.append("commitOnClose=").append(getCommitOnClose()).append("\n");
sb.append("indexSort=").append(getIndexSort()).append("\n");
sb.append("checkPendingFlushOnUpdate=").append(isCheckPendingFlushOnUpdate()).append("\n");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ void add(int doc, BytesRef value) {

@Override
synchronized void reset(int doc) {
anyReset = true;
bitSet.set(doc);
this.hasAtLeastOneValue = true;
if (hasNoValue == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.index;

import java.io.IOException;
import org.apache.lucene.util.BytesRef;

/** Binary counterpart of {@link OverlayNumericDocValues}; see it for the layering and invariant. */
final class OverlayBinaryDocValues extends BinaryDocValues {

private final BinaryDocValues[] layers; // newest first, base last
private final DocValuesOverlayMerger merger;

OverlayBinaryDocValues(BinaryDocValues[] layers) {
assert layers != null && layers.length > 0;
this.layers = layers;
this.merger = new DocValuesOverlayMerger(layers);
}

@Override
public BytesRef binaryValue() throws IOException {
return layers[merger.valueLayer()].binaryValue();
}

@Override
public boolean advanceExact(int target) throws IOException {
return merger.advanceExact(target);
}

@Override
public int docID() {
return merger.docID();
}

@Override
public int nextDoc() throws IOException {
return merger.nextDoc();
}

@Override
public int advance(int target) throws IOException {
return merger.advance(target);
}

@Override
public long cost() {
return merger.cost();
}
}
Loading
Loading