blob: 21aac962fc9653925f8837e5818ce19bffec49cf [file] [log] [blame]
/*
* 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 java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.lucene.document.Field;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.DocValuesFieldExistsQuery;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.FixedBitSet;
/**
* This reader filters out documents that have a doc values value in the given field and treat these
* documents as soft deleted. Hard deleted documents will also be filtered out in the life docs of this reader.
* @see IndexWriterConfig#setSoftDeletesField(String)
* @see IndexWriter#softUpdateDocument(Term, Iterable, Field...)
* @see SoftDeletesRetentionMergePolicy
*/
public final class SoftDeletesDirectoryReaderWrapper extends FilterDirectoryReader {
private final String field;
private final CacheHelper readerCacheHelper;
/**
* Creates a new soft deletes wrapper.
* @param in the incoming directory reader
* @param field the soft deletes field
*/
public SoftDeletesDirectoryReaderWrapper(DirectoryReader in, String field) throws IOException {
this(in, new SoftDeletesSubReaderWrapper(Collections.emptyMap(), field));
}
private SoftDeletesDirectoryReaderWrapper(DirectoryReader in, SoftDeletesSubReaderWrapper wrapper) throws IOException {
super(in, wrapper);
this.field = wrapper.field;
readerCacheHelper = in.getReaderCacheHelper() == null ? null : new DelegatingCacheHelper(in.getReaderCacheHelper());
}
@Override
protected DirectoryReader doWrapDirectoryReader(DirectoryReader in) throws IOException {
Map<CacheKey, LeafReader> readerCache = new HashMap<>();
for (LeafReader reader : getSequentialSubReaders()) {
// we try to reuse the life docs instances here if the reader cache key didn't change
if (reader instanceof SoftDeletesFilterLeafReader && reader.getReaderCacheHelper() != null) {
readerCache.put(((SoftDeletesFilterLeafReader) reader).reader.getReaderCacheHelper().getKey(), reader);
} else if (reader instanceof SoftDeletesFilterCodecReader && reader.getReaderCacheHelper() != null) {
readerCache.put(((SoftDeletesFilterCodecReader) reader).reader.getReaderCacheHelper().getKey(), reader);
}
}
return new SoftDeletesDirectoryReaderWrapper(in, new SoftDeletesSubReaderWrapper(readerCache, field));
}
@Override
public CacheHelper getReaderCacheHelper() {
return readerCacheHelper;
}
private static class SoftDeletesSubReaderWrapper extends SubReaderWrapper {
private final Map<CacheKey, LeafReader> mapping;
private final String field;
public SoftDeletesSubReaderWrapper(Map<CacheKey, LeafReader> oldReadersCache, String field) {
Objects.requireNonNull(field, "Field must not be null");
assert oldReadersCache != null;
this.mapping = oldReadersCache;
this.field = field;
}
protected LeafReader[] wrap(List<? extends LeafReader> readers) {
List<LeafReader> wrapped = new ArrayList<>(readers.size());
for (LeafReader reader : readers) {
LeafReader wrap = wrap(reader);
assert wrap != null;
if (wrap.numDocs() != 0) {
wrapped.add(wrap);
}
}
return wrapped.toArray(new LeafReader[0]);
}
@Override
public LeafReader wrap(LeafReader reader) {
CacheHelper readerCacheHelper = reader.getReaderCacheHelper();
if (readerCacheHelper != null && mapping.containsKey(readerCacheHelper.getKey())) {
// if the reader cache helper didn't change and we have it in the cache don't bother creating a new one
return mapping.get(readerCacheHelper.getKey());
}
try {
return SoftDeletesDirectoryReaderWrapper.wrap(reader, field);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
static LeafReader wrap(LeafReader reader, String field) throws IOException {
DocIdSetIterator iterator = DocValuesFieldExistsQuery.getDocValuesDocIdSetIterator(field, reader);
if (iterator == null) {
return reader;
}
Bits liveDocs = reader.getLiveDocs();
final FixedBitSet bits;
if (liveDocs != null) {
bits = FixedBitSet.copyOf(liveDocs);
} else {
bits = new FixedBitSet(reader.maxDoc());
bits.set(0, reader.maxDoc());
}
int numSoftDeletes = PendingSoftDeletes.applySoftDeletes(iterator, bits);
int numDeletes = reader.numDeletedDocs() + numSoftDeletes;
int numDocs = reader.maxDoc() - numDeletes;
assert assertDocCounts(numDocs, numSoftDeletes, reader);
return reader instanceof CodecReader ? new SoftDeletesFilterCodecReader((CodecReader) reader, bits, numDocs)
: new SoftDeletesFilterLeafReader(reader, bits, numDocs);
}
private static boolean assertDocCounts(int expectedNumDocs, int numSoftDeletes, LeafReader reader) {
if (reader instanceof SegmentReader) {
SegmentReader segmentReader = (SegmentReader) reader;
SegmentCommitInfo segmentInfo = segmentReader.getSegmentInfo();
if (segmentReader.isNRT == false) {
int numDocs = segmentInfo.info.maxDoc() - segmentInfo.getSoftDelCount() - segmentInfo.getDelCount();
assert numDocs == expectedNumDocs : "numDocs: " + numDocs + " expected: " + expectedNumDocs
+ " maxDoc: " + segmentInfo.info.maxDoc()
+ " getDelCount: " + segmentInfo.getDelCount()
+ " getSoftDelCount: " + segmentInfo.getSoftDelCount()
+ " numSoftDeletes: " + numSoftDeletes
+ " reader.numDeletedDocs(): " + reader.numDeletedDocs();
}
// in the NRT case we don't have accurate numbers for getDelCount and getSoftDelCount since they might not be
// flushed to disk when this reader is opened. We don't necessarily flush deleted doc on reopen but
// we do for docValues.
}
return true;
}
static final class SoftDeletesFilterLeafReader extends FilterLeafReader {
private final LeafReader reader;
private final FixedBitSet bits;
private final int numDocs;
private final CacheHelper readerCacheHelper;
private SoftDeletesFilterLeafReader(LeafReader reader, FixedBitSet bits, int numDocs) {
super(reader);
this.reader = reader;
this.bits = bits;
this.numDocs = numDocs;
this.readerCacheHelper = reader.getReaderCacheHelper() == null ? null :
new DelegatingCacheHelper(reader.getReaderCacheHelper());
}
@Override
public Bits getLiveDocs() {
return bits;
}
@Override
public int numDocs() {
return numDocs;
}
@Override
public CacheHelper getCoreCacheHelper() {
return reader.getCoreCacheHelper();
}
@Override
public CacheHelper getReaderCacheHelper() {
return readerCacheHelper;
}
}
final static class SoftDeletesFilterCodecReader extends FilterCodecReader {
private final LeafReader reader;
private final FixedBitSet bits;
private final int numDocs;
private final CacheHelper readerCacheHelper;
private SoftDeletesFilterCodecReader(CodecReader reader, FixedBitSet bits, int numDocs) {
super(reader);
this.reader = reader;
this.bits = bits;
this.numDocs = numDocs;
this.readerCacheHelper = reader.getReaderCacheHelper() == null ? null :
new DelegatingCacheHelper(reader.getReaderCacheHelper());
}
@Override
public Bits getLiveDocs() {
return bits;
}
@Override
public int numDocs() {
return numDocs;
}
@Override
public CacheHelper getCoreCacheHelper() {
return reader.getCoreCacheHelper();
}
@Override
public CacheHelper getReaderCacheHelper() {
return readerCacheHelper;
}
}
private static class DelegatingCacheHelper implements CacheHelper {
private final CacheHelper delegate;
private final CacheKey cacheKey = new CacheKey();
public DelegatingCacheHelper(CacheHelper delegate) {
this.delegate = delegate;
}
@Override
public CacheKey getKey() {
return cacheKey;
}
@Override
public void addClosedListener(ClosedListener listener) {
// here we wrap the listener and call it with our cache key
// this is important since this key will be used to cache the reader and otherwise we won't free caches etc.
delegate.addClosedListener(unused -> listener.onClose(cacheKey));
}
}
}