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" overlay holding just the updated documents and layered over 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; disable with IndexWriterConfig#setMaxDocValuesOverlays(0). (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,13 @@ public enum OpenMode {
*/
public static final long DEFAULT_MAX_FULL_FLUSH_MERGE_WAIT_MILLIS = 500;

/**
* Default maximum number of sparse doc-values overlays a field keeps before they are folded into
* one: {@code 16}. {@code 0} disables the feature (classic full-column rewrite); higher values
* lower write amplification but keep more overlays (hence more files) live to merge at read time.
*/
public static final int DEFAULT_MAX_DOC_VALUES_OVERLAYS = 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 +331,29 @@ public boolean getReaderPooling() {
return readerPooling;
}

/**
* Sets the maximum number of sparse doc-values overlays a field may accumulate before they are
* folded into a single overlay, which also enables or disables the feature. When enabled, a
* doc-values update that only sets values (no removals) is written as a sparse "delta" 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. Pass {@code 0} to disable the feature and keep the classic
* full-column rewrite; any value {@code > 0} enables it, keeping up to that many overlays before
* a fold. Enabled by default (see {@link #DEFAULT_MAX_DOC_VALUES_OVERLAYS}).
*
* <p>Only takes effect when IndexWriter is first created.
*
* @lucene.experimental
*/
public IndexWriterConfig setMaxDocValuesOverlays(int maxDocValuesOverlays) {
if (maxDocValuesOverlays < 0) {
throw new IllegalArgumentException(
"maxDocValuesOverlays must be >= 0; got " + maxDocValuesOverlays);
}
this.maxDocValuesOverlays = maxDocValuesOverlays;
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;

/**
* Maximum number of sparse doc-values overlays a field keeps before they are folded into one;
* {@code 0} disables the feature. See {@link IndexWriterConfig#setMaxDocValuesOverlays}.
*/
protected int maxDocValuesOverlays;

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

Expand Down Expand Up @@ -137,6 +143,7 @@ public class LiveIndexWriterConfig {
mergePolicy = new TieredMergePolicy();
flushPolicy = new FlushByRamOrCountsPolicy();
readerPooling = IndexWriterConfig.DEFAULT_READER_POOLING;
maxDocValuesOverlays = IndexWriterConfig.DEFAULT_MAX_DOC_VALUES_OVERLAYS;
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 +392,16 @@ public boolean getUseCompoundFile() {
return useCompoundFile;
}

/**
* Returns the maximum number of sparse doc-values overlays a field keeps before they are folded
* into one, or {@code 0} if the feature is disabled.
*
* @lucene.experimental
*/
public int getMaxDocValuesOverlays() {
return maxDocValuesOverlays;
}

/**
* Returns <code>true</code> if {@link IndexWriter#close()} should first commit before closing.
*/
Expand Down Expand Up @@ -487,6 +504,7 @@ 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("maxDocValuesOverlays=").append(getMaxDocValuesOverlays()).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