This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 32bc2ee2da7 Prebuild Lucene doc ID mappings from numeric doc values
(#19897)
32bc2ee2da7 is described below
commit 32bc2ee2da7e91f29bc61d8154abe5b652cd7cda
Author: Xiang Fu <[email protected]>
AuthorDate: Mon Oct 26 14:28:31 2026 -0700
Prebuild Lucene doc ID mappings from numeric doc values (#19897)
Build Lucene-to-Pinot mappings during immutable TEXT index creation,
persisting Skip for identity mappings and packaging reordered mappings. Retain
legacy stored-field fallback; new indexes require Pinot 1.4.0 or later readers.
---
.../MultiColumnRealtimeLuceneTextIndex.java | 2 +-
.../invertedindex/RealtimeLuceneTextIndex.java | 2 +-
.../creator/impl/text/LuceneTextIndexCreator.java | 112 ++++----
.../index/readers/text/LuceneTextIndexReader.java | 164 +++---------
.../text/MultiColumnLuceneTextIndexReader.java | 4 +-
.../local/segment/store/TextIndexUtils.java | 68 +++++
.../index/creator/LuceneTextIndexCreatorTest.java | 287 +++++++++++++++++++--
.../local/segment/index/loader/LoaderTest.java | 54 ++--
.../TextIndexRollbackRecoveryTest.java | 2 +-
.../text/LuceneTextIndexCompatibleTest.java | 174 ++++++++++++-
.../readers/text/LuceneTextIndexCountTest.java | 22 +-
.../segment/store/FilePerIndexDirectoryTest.java | 22 +-
.../store/SingleFileIndexDirectoryTest.java | 22 +-
.../pinot/segment/spi/index/TextIndexConfig.java | 6 +-
14 files changed, 675 insertions(+), 266 deletions(-)
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/invertedindex/MultiColumnRealtimeLuceneTextIndex.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/invertedindex/MultiColumnRealtimeLuceneTextIndex.java
index ba7f0a2f205..a7c84e31d93 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/invertedindex/MultiColumnRealtimeLuceneTextIndex.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/invertedindex/MultiColumnRealtimeLuceneTextIndex.java
@@ -290,7 +290,7 @@ public class MultiColumnRealtimeLuceneTextIndex implements
MultiColumnTextIndexR
while (luceneDocIDIterator.hasNext()) {
int luceneDocId = luceneDocIDIterator.next();
Document document = indexSearcher.doc(luceneDocId);
- int pinotDocId =
Integer.parseInt(document.get(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME));
+ int pinotDocId =
Integer.parseInt(document.get(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME));
actualDocIDs.add(pinotDocId);
}
} catch (Exception e) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/invertedindex/RealtimeLuceneTextIndex.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/invertedindex/RealtimeLuceneTextIndex.java
index 9fe514de892..8033139dbf6 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/invertedindex/RealtimeLuceneTextIndex.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/invertedindex/RealtimeLuceneTextIndex.java
@@ -258,7 +258,7 @@ public class RealtimeLuceneTextIndex implements
MutableTextIndex {
while (luceneDocIDIterator.hasNext()) {
int luceneDocId = luceneDocIDIterator.next();
Document document = indexSearcher.doc(luceneDocId);
- int pinotDocId =
Integer.parseInt(document.get(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME));
+ int pinotDocId =
Integer.parseInt(document.get(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME));
actualDocIDs.add(pinotDocId);
}
} catch (Exception e) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/text/LuceneTextIndexCreator.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/text/LuceneTextIndexCreator.java
index 359e41d31d7..a144886426f 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/text/LuceneTextIndexCreator.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/text/LuceneTextIndexCreator.java
@@ -29,6 +29,7 @@ import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.CharArraySet;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
+import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
@@ -36,16 +37,17 @@ import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.LogByteSizeMergePolicy;
-import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.NRTCachingDirectory;
import
org.apache.pinot.segment.local.realtime.impl.invertedindex.LuceneNRTCachingMergePolicy;
import
org.apache.pinot.segment.local.segment.index.text.AbstractTextIndexCreator;
+import
org.apache.pinot.segment.local.segment.index.text.TextIndexConfigBuilder;
import org.apache.pinot.segment.local.segment.store.TextIndexUtils;
import org.apache.pinot.segment.spi.V1Constants;
import org.apache.pinot.segment.spi.creator.IndexCreationContext;
import org.apache.pinot.segment.spi.index.TextIndexConfig;
+import org.apache.pinot.segment.spi.index.TextIndexConfig.DocIdTranslatorMode;
import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
import org.apache.pinot.segment.spi.store.SegmentDirectoryPaths;
import org.slf4j.Logger;
@@ -57,7 +59,13 @@ import org.slf4j.LoggerFactory;
/// and realtime from
[org.apache.pinot.segment.local.realtime.impl.invertedindex.RealtimeLuceneTextIndex]
public class LuceneTextIndexCreator extends AbstractTextIndexCreator {
private static final Logger LOGGER =
LoggerFactory.getLogger(LuceneTextIndexCreator.class);
- public static final String LUCENE_INDEX_DOC_ID_COLUMN_NAME = "DocID";
+ /// Stored Pinot document ID used by mutable and legacy immutable indexes.
+ /// Also the alternate numeric doc-values field when the TEXT column is
named "PinotDocID".
+ public static final String PINOT_DOC_ID_STORED_FIELD_NAME = "DocID";
+
+ /// Preferred numeric doc-values field holding the Pinot document ID in
freshly built immutable indexes.
+ /// Used to build the Lucene-to-Pinot mapping; omitted when explicitly
configured with Skip.
+ public static final String PINOT_DOC_ID_DOC_VALUES_FIELD_NAME = "PinotDocID";
private final String _textColumn;
private final boolean _commitOnClose;
@@ -89,7 +97,7 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
/// been added), false if index should not be committed
/// @param realtimeConversion index creator should create an index using the
realtime segment
/// @param consumerDir consumer dir containing the realtime index, used when
realtimeConversion and commit is true
- /// @param immutableToMutableIdMap immutableToMutableIdMap from segment
conversion
+ /// @param mutableToImmutableDocIdMap mutableToImmutableDocIdMap from
segment conversion
/// @param combineAndCleanupFiles true if files should be combined and text
index directory should be removed
/// Note on commit:
/// Once
[org.apache.pinot.segment.local.segment.creator.impl.SegmentColumnarIndexCreator]
@@ -108,7 +116,7 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
/// to offline), we close this lucene index writer to release
resources but don't commit.
/// @param config the text index config
public LuceneTextIndexCreator(String column, File segmentIndexDir, boolean
commit, boolean realtimeConversion,
- @Nullable File consumerDir, boolean mutableSegmentCompacted, @Nullable
int[] immutableToMutableIdMap,
+ @Nullable File consumerDir, boolean mutableSegmentCompacted, @Nullable
int[] mutableToImmutableDocIdMap,
boolean combineAndCleanupFiles, TextIndexConfig config) {
_textColumn = column;
_commitOnClose = commit;
@@ -149,7 +157,7 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
if (_reuseMutableIndex) {
LOGGER.info("Reusing the realtime lucene index for segment {} and
column {}", segmentIndexDir, column);
indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
- convertMutableSegment(segmentIndexDir, consumerDir,
immutableToMutableIdMap, indexWriterConfig);
+ convertMutableSegment(segmentIndexDir, consumerDir,
mutableToImmutableDocIdMap, indexWriterConfig);
return;
}
@@ -189,11 +197,11 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
/// @param commit true if the index should be committed
/// @param realtimeConversion index creator should create an index using the
realtime segment
/// @param consumerDir consumer dir containing the realtime index
- /// @param immutableToMutableIdMap immutableToMutableIdMap from segment
conversion
+ /// @param mutableToImmutableDocIdMap mutableToImmutableDocIdMap from
segment conversion
/// @param config the text index config
public LuceneTextIndexCreator(String column, File segmentIndexDir, boolean
commit, boolean realtimeConversion,
- @Nullable File consumerDir, @Nullable int[] immutableToMutableIdMap,
TextIndexConfig config) {
- this(column, segmentIndexDir, commit, realtimeConversion, consumerDir,
false, immutableToMutableIdMap, false,
+ @Nullable File consumerDir, @Nullable int[] mutableToImmutableDocIdMap,
TextIndexConfig config) {
+ this(column, segmentIndexDir, commit, realtimeConversion, consumerDir,
false, mutableToImmutableDocIdMap, false,
config);
}
@@ -237,15 +245,15 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
indexWriter.commit();
buildMappingFile(segmentIndexDir, _textColumn, destDirectory,
mutableToImmutableDocIdMap);
} catch (Exception e) {
- throw new RuntimeException("Failed to build the mapping file during
segment conversion: " + e);
+ throw new RuntimeException("Failed to build the mapping file during
segment conversion", e);
}
} catch (IOException e) {
throw new RuntimeException("Failed to convert the mutable lucene index:
" + e);
}
}
- /// Generate the mapping file from mutable Pinot docId (stored within the
Lucene index) to immutable Pinot docId using
- /// the mutableToImmutableDocIdMap from segment conversion
+ /// Build a mapping from final Lucene ids to Pinot ids, applying
mutable-to-immutable remapping during conversion.
+ /// New immutable indexes persist either the mapping or a verified Skip
setting for Pinot 1.4.0 and later readers.
/// @param segmentIndexDir segment index directory
/// @param column column name
/// @param directory directory of the index
@@ -253,39 +261,31 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
private void buildMappingFile(File segmentIndexDir, String column, Directory
directory,
@Nullable int[] mutableToImmutableDocIdMap)
throws IOException {
- IndexReader indexReader = DirectoryReader.open(directory);
- IndexSearcher indexSearcher = new IndexSearcher(indexReader);
-
- int numDocs = indexSearcher.getIndexReader().numDocs();
- int length = Integer.BYTES * numDocs;
- File docIdMappingFile = new
File(SegmentDirectoryPaths.findSegmentDirectory(segmentIndexDir),
- column +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
- String desc = "Text index docId mapping buffer: " + column;
- try (PinotDataBuffer buffer = PinotDataBuffer.mapFile(docIdMappingFile, /*
readOnly */ false, 0, length,
- ByteOrder.LITTLE_ENDIAN, desc)) {
- try {
- // If immutableToMutableIdMap is null, then docIds should not change
between the mutable and immutable segments.
- // Therefore, the mapping file can be built without doing an
additional docId conversion
- if (mutableToImmutableDocIdMap == null) {
- for (int i = 0; i < numDocs; i++) {
- Document document = indexSearcher.storedFields().document(i);
- int pinotDocId =
Integer.parseInt(document.get(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME));
- buffer.putInt(i * Integer.BYTES, pinotDocId);
- }
- } else {
- for (int i = 0; i < numDocs; i++) {
- Document document = indexSearcher.storedFields().document(i);
- int mutableDocId =
Integer.parseInt(document.get(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME));
- int immutableDocId = mutableToImmutableDocIdMap[mutableDocId];
- buffer.putInt(i * Integer.BYTES, immutableDocId);
- }
- }
+ if (_config.getDocIdTranslatorMode() == DocIdTranslatorMode.Skip) {
+ return;
+ }
+ try (IndexReader indexReader = DirectoryReader.open(directory)) {
+ int numDocs = indexReader.numDocs();
+ int length = Integer.BYTES * numDocs;
+ File docIdMappingFile = new
File(SegmentDirectoryPaths.findSegmentDirectory(segmentIndexDir),
+ column +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
+ String desc = "Text index docId mapping buffer: " + column;
+ boolean isIdentityMapping;
+ try (PinotDataBuffer buffer = PinotDataBuffer.mapFile(docIdMappingFile,
/* readOnly */ false, 0, length,
+ ByteOrder.LITTLE_ENDIAN, desc)) {
+ isIdentityMapping = TextIndexUtils.fillDocIdMapping(indexReader,
buffer, numDocs, mutableToImmutableDocIdMap);
} catch (Exception e) {
throw new RuntimeException(
"Caught exception while building mutable to immutable doc id
mapping for text index column: " + column, e);
}
- } finally {
- indexReader.close();
+ if (!_reuseMutableIndex || _config.getDocIdTranslatorMode() ==
DocIdTranslatorMode.TryOptimize) {
+ if (isIdentityMapping) {
+ FileUtils.delete(docIdMappingFile);
+ }
+ DocIdTranslatorMode mode = isIdentityMapping ?
DocIdTranslatorMode.Skip : DocIdTranslatorMode.Default;
+ TextIndexConfig newConfig = new
TextIndexConfigBuilder(_config).withDocIdTranslatorMode(mode.name()).build();
+ TextIndexUtils.writeConfigToPropertiesFile(_indexFile, newConfig);
+ }
}
}
@@ -298,7 +298,7 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
// text index on SV column
Document docToIndex = new Document();
docToIndex.add(new TextField(_textColumn, document, Field.Store.NO));
- docToIndex.add(new StoredField(LUCENE_INDEX_DOC_ID_COLUMN_NAME,
_nextDocId++));
+ addDocIdFields(docToIndex);
try {
_indexWriter.addDocument(docToIndex);
} catch (Exception e) {
@@ -321,7 +321,7 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
for (int i = 0; i < length; i++) {
docToIndex.add(new TextField(_textColumn, documents[i], Field.Store.NO));
}
- docToIndex.add(new StoredField(LUCENE_INDEX_DOC_ID_COLUMN_NAME,
_nextDocId++));
+ addDocIdFields(docToIndex);
try {
_indexWriter.addDocument(docToIndex);
@@ -331,6 +331,20 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
}
}
+ private void addDocIdFields(Document document) {
+ int docId = _nextDocId++;
+ if (!_commitOnClose) {
+ // Mutable indexes retain the stored field for existing readers and
segment conversion.
+ document.add(new StoredField(PINOT_DOC_ID_STORED_FIELD_NAME, docId));
+ } else if (_config.getDocIdTranslatorMode() != DocIdTranslatorMode.Skip) {
+ // Avoid sharing the numeric field with the TEXT column, including empty
multi-value documents.
+ String fieldName = _textColumn.equals(PINOT_DOC_ID_DOC_VALUES_FIELD_NAME)
+ ? PINOT_DOC_ID_STORED_FIELD_NAME
+ : PINOT_DOC_ID_DOC_VALUES_FIELD_NAME;
+ document.add(new NumericDocValuesField(fieldName, docId));
+ }
+ }
+
@Override
public void seal() {
if (_reuseMutableIndex) {
@@ -408,17 +422,9 @@ public class LuceneTextIndexCreator extends
AbstractTextIndexCreator {
try {
// based on the commit flag set in IndexWriterConfig, this will decide
to commit or not
_indexWriter.close();
- // Build docIdMapping file if storeInSegmentFile is true
- // This allows the mapping file to be available during read without
building it on-the-fly
- if (_config.isStoreInSegmentFile()) {
- //Check if mapping file already exists
- File mappingFile = new
File(SegmentDirectoryPaths.findSegmentDirectory(_segmentDirectory),
- _textColumn +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
- if (!mappingFile.exists()) {
- LOGGER.info("lucene doc IdMapping file doesn't exists for column:
{}, building mapping file", _textColumn);
- // Build the docId mapping file so it's available during segment load
- buildMappingFile(_segmentDirectory, _textColumn, _indexDirectory,
null);
- }
+ if (_commitOnClose) {
+ // Older readers require the final mapping or verified Skip setting
when the stored DocID is absent.
+ buildMappingFile(_segmentDirectory, _textColumn, _indexDirectory,
null);
}
_indexDirectory.close();
} catch (Exception e) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexReader.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexReader.java
index 6e73e11f10d..77e79cabafb 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexReader.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexReader.java
@@ -29,13 +29,8 @@ import java.util.Map;
import java.util.Properties;
import javax.annotation.Nullable;
import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.DocumentStoredFieldVisitor;
import org.apache.lucene.index.DirectoryReader;
-import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.index.IndexableField;
-import org.apache.lucene.index.StoredFields;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.queryparser.classic.QueryParserBase;
import org.apache.lucene.search.Collector;
@@ -43,7 +38,6 @@ import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
-import
org.apache.pinot.segment.local.segment.creator.impl.text.LuceneTextIndexCreator;
import
org.apache.pinot.segment.local.segment.index.text.TextIndexConfigBuilder;
import org.apache.pinot.segment.local.segment.store.TextIndexUtils;
import org.apache.pinot.segment.local.utils.LuceneTextIndexUtils;
@@ -242,6 +236,7 @@ public class LuceneTextIndexReader implements
TextIndexReader {
MutableRoaringBitmap docIds = new MutableRoaringBitmap();
Collector docIDCollector = new LuceneDocIdCollector(docIds,
_docIdTranslator);
try {
+ validateDocCount();
_indexSearcher.search(buildQuery(searchQuery, optionsString,
queryScopedCache), docIDCollector);
return docIds;
} catch (QueryException e) {
@@ -257,6 +252,15 @@ public class LuceneTextIndexReader implements
TextIndexReader {
}
}
+ private void validateDocCount() {
+ int maxDoc = _indexReader.maxDoc();
+ // Collectors address maxDoc() space, including deleted documents. Skip
mode has no mapping-buffer bounds check.
+ if (maxDoc > _numDocs) {
+ throw new IllegalStateException(
+ "Lucene text index contains " + maxDoc + " documents, exceeding
segment document count: " + _numDocs);
+ }
+ }
+
/// Identifies a parsing configuration. A record so that equality is
structural: comparing the
/// rendered text of these lists instead would conflate `["failed",
"order"]` with
/// `["failed, order"]`, which are different stop-word sets that parse the
same string differently.
@@ -328,15 +332,7 @@ public class LuceneTextIndexReader implements
TextIndexReader {
public int getNumMatchingDocs(String searchQuery, @Nullable String
optionsString,
@Nullable QueryScopedCache queryScopedCache) {
try {
- // A Lucene index larger than the segment means the doc-id mapping is
broken -- a state getDocIds
- // surfaces as an IndexOutOfBoundsException from the translator.
Counting does not consult the
- // translator, so without this guard the corruption would become a
silently inflated count instead.
- // Compared against maxDoc(), not numDocs(): LuceneDocIdCollector
indexes the translator by
- // `context.docBase + doc`, which is maxDoc() space, and with deletions
numDocs() < maxDoc() would let a
- // broken index slip through. Fall back so that it keeps failing loudly.
- if (_indexReader.maxDoc() > _numDocs) {
- return getDocIds(searchQuery, optionsString,
queryScopedCache).getCardinality();
- }
+ validateDocCount();
return LuceneTextIndexUtils.countWithoutMaterializing(_indexSearcher,
buildQuery(searchQuery, optionsString, queryScopedCache));
} catch (QueryException e) {
@@ -479,73 +475,35 @@ public class LuceneTextIndexReader implements
TextIndexReader {
PinotDataBuffer.mapFile(docIdMappingFile, /* readOnly */ true, 0,
length, ByteOrder.LITTLE_ENDIAN, desc);
return new DefaultDocIdTranslator(buffer);
- } else {
- buffer =
- PinotDataBuffer.mapFile(docIdMappingFile, /* readOnly */ false, 0,
length, ByteOrder.LITTLE_ENDIAN, desc);
-
- if (config.getDocIdTranslatorMode() ==
DocIdTranslatorMode.TryOptimize) {
- LOGGER.debug("Creating lucene to pinot doc id mapping.");
- boolean allIdsAreEqual = true;
-
- class DocIdVisitor extends DocumentStoredFieldVisitor {
- int _pinotDocId;
-
- @Override
- public Status needsField(FieldInfo fieldInfo) {
- // assume doc id is the only stored document
- assert
LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME.equals(fieldInfo.name);
- return Status.YES;
- }
-
- @Override
- public void intField(FieldInfo fieldInfo, int value) {
- _pinotDocId = value;
- }
- }
-
- DocIdVisitor visitor = new DocIdVisitor();
- StoredFields storedFields = indexSearcher.storedFields();
-
- for (int i = 0; i < numDocs; i++) {
- storedFields.document(i, visitor);
- int pinotDocId = visitor._pinotDocId;
- allIdsAreEqual &= (i == pinotDocId);
- buffer.putInt(i * Integer.BYTES, pinotDocId);
- }
-
- if (allIdsAreEqual) {
- LOGGER.debug("Lucene doc ids are equal to Pinot's. Deleting
mapping and updating index settings.");
- // TODO: it'd be better to unmap without flushing the buffer. Only
some buffer types support it, though.
- buffer.close();
- buffer = null;
- // get rid of mapping and use lucene ids
- docIdMappingFile.delete();
-
- // mapping is unnecessary so store flag in config file to skip
checking on next load
- TextIndexConfig newConfig =
- new
TextIndexConfigBuilder(config).withDocIdTranslatorMode(DocIdTranslatorMode.Skip.name()).build();
- TextIndexUtils.writeConfigToPropertiesFile(indexDir, newConfig);
-
- return NoOpDocIdTranslator.INSTANCE;
- } else {
- LOGGER.debug("Lucene doc ids are not equal to Pinot's. Keeping the
mapping.");
- // mapping is required so switch to default mode
- TextIndexConfig newConfig =
- new
TextIndexConfigBuilder(config).withDocIdTranslatorMode(DocIdTranslatorMode.Default.name()).build();
- TextIndexUtils.writeConfigToPropertiesFile(indexDir, newConfig);
-
- return new DefaultDocIdTranslator(buffer);
- }
- } else {
- for (int i = 0; i < numDocs; i++) {
- Document document = indexSearcher.doc(i);
- IndexableField field =
document.getField(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME);
- int pinotDocId = Integer.parseInt(field.stringValue());
- buffer.putInt(i * Integer.BYTES, pinotDocId);
- }
- return new DefaultDocIdTranslator(buffer);
+ }
+ buffer =
+ PinotDataBuffer.mapFile(docIdMappingFile, /* readOnly */ false, 0,
length, ByteOrder.LITTLE_ENDIAN, desc);
+ LOGGER.debug("Creating lucene to pinot doc id mapping.");
+ boolean isIdentityMapping =
+ TextIndexUtils.fillDocIdMapping(indexSearcher.getIndexReader(),
buffer, numDocs, null);
+ if (config.getDocIdTranslatorMode() == DocIdTranslatorMode.TryOptimize) {
+ if (isIdentityMapping) {
+ LOGGER.debug("Lucene doc ids are equal to Pinot's. Deleting mapping
and updating index settings.");
+ // TODO: it'd be better to unmap without flushing the buffer. Only
some buffer types support it, though.
+ buffer.close();
+ buffer = null;
+ // get rid of mapping and use lucene ids
+ docIdMappingFile.delete();
+
+ // mapping is unnecessary so store flag in config file to skip
checking on next load
+ TextIndexConfig newConfig =
+ new
TextIndexConfigBuilder(config).withDocIdTranslatorMode(DocIdTranslatorMode.Skip.name()).build();
+ TextIndexUtils.writeConfigToPropertiesFile(indexDir, newConfig);
+
+ return NoOpDocIdTranslator.INSTANCE;
}
+ LOGGER.debug("Lucene doc ids are not equal to Pinot's. Keeping the
mapping.");
+ // mapping is required so switch to default mode
+ TextIndexConfig newConfig =
+ new
TextIndexConfigBuilder(config).withDocIdTranslatorMode(DocIdTranslatorMode.Default.name()).build();
+ TextIndexUtils.writeConfigToPropertiesFile(indexDir, newConfig);
}
+ return new DefaultDocIdTranslator(buffer);
} catch (Exception e) {
if (buffer != null) {
buffer.close();
@@ -579,53 +537,17 @@ public class LuceneTextIndexReader implements
TextIndexReader {
PinotDataBuffer buffer = PinotDataBuffer.allocateDirect(length,
ByteOrder.LITTLE_ENDIAN, desc);
try {
+ LOGGER.debug("Creating lucene to pinot doc id mapping.");
+ boolean isIdentityMapping =
TextIndexUtils.fillDocIdMapping(_indexReader, buffer, numDocs, null);
if (config.getDocIdTranslatorMode() == DocIdTranslatorMode.TryOptimize) {
- LOGGER.debug("Creating lucene to pinot doc id mapping.");
- boolean allIdsAreEqual = true;
-
- class DocIdVisitor extends DocumentStoredFieldVisitor {
- int _pinotDocId;
-
- @Override
- public Status needsField(FieldInfo fieldInfo) {
- // assume doc id is the only stored document
- assert
LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME.equals(fieldInfo.name);
- return Status.YES;
- }
-
- @Override
- public void intField(FieldInfo fieldInfo, int value) {
- _pinotDocId = value;
- }
- }
-
- DocIdVisitor visitor = new DocIdVisitor();
- StoredFields storedFields = _indexSearcher.storedFields();
-
- for (int i = 0; i < numDocs; i++) {
- storedFields.document(i, visitor);
- int pinotDocId = visitor._pinotDocId;
- allIdsAreEqual &= (i == pinotDocId);
- buffer.putInt(i * Integer.BYTES, pinotDocId);
- }
-
- if (allIdsAreEqual) {
+ if (isIdentityMapping) {
LOGGER.debug("Lucene doc ids are equal to Pinot's. Using no-op
translator.");
buffer.close();
return NoOpDocIdTranslator.INSTANCE;
- } else {
- LOGGER.debug("Lucene doc ids are not equal to Pinot's. Using default
translator.");
- return new DefaultDocIdTranslator(buffer);
}
- } else {
- for (int i = 0; i < numDocs; i++) {
- Document document = _indexSearcher.doc(i);
- IndexableField field =
document.getField(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME);
- int pinotDocId = Integer.parseInt(field.stringValue());
- buffer.putInt(i * Integer.BYTES, pinotDocId);
- }
- return new DefaultDocIdTranslator(buffer);
+ LOGGER.debug("Lucene doc ids are not equal to Pinot's. Using default
translator.");
}
+ return new DefaultDocIdTranslator(buffer);
} catch (Exception e) {
buffer.close();
throw new RuntimeException("Caught exception while creating doc id
translator for text index column: " + _column,
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/text/MultiColumnLuceneTextIndexReader.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/text/MultiColumnLuceneTextIndexReader.java
index 9388798250f..adeba3b9a3c 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/text/MultiColumnLuceneTextIndexReader.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/text/MultiColumnLuceneTextIndexReader.java
@@ -475,7 +475,7 @@ public class MultiColumnLuceneTextIndexReader implements
MultiColumnTextIndexRea
@Override
public Status needsField(FieldInfo fieldInfo) {
// assume doc id is the only stored document
- assert
LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME.equals(fieldInfo.name);
+ assert
LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME.equals(fieldInfo.name);
return Status.YES;
}
@@ -521,7 +521,7 @@ public class MultiColumnLuceneTextIndexReader implements
MultiColumnTextIndexRea
} else {
for (int i = 0; i < numDocs; i++) {
Document document = indexSearcher.storedFields().document(i);
- IndexableField field =
document.getField(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME);
+ IndexableField field =
document.getField(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME);
int pinotDocId = Integer.parseInt(field.stringValue());
buffer.putInt(i * Integer.BYTES, pinotDocId);
}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/TextIndexUtils.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/TextIndexUtils.java
index 3f97bc15618..e7addf0e5f0 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/TextIndexUtils.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/TextIndexUtils.java
@@ -36,6 +36,12 @@ import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.CharArraySet;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.MultiDocValues;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.index.StoredFieldVisitor;
+import org.apache.lucene.index.StoredFields;
import org.apache.lucene.queryparser.classic.QueryParserBase;
import org.apache.lucene.search.IndexSearcher;
import
org.apache.pinot.segment.local.segment.creator.impl.text.LuceneTextIndexCreator;
@@ -89,6 +95,68 @@ public class TextIndexUtils {
}
}
+ /// Reads the actual Pinot ids after Lucene merges and sorting, optionally
remapping mutable ids during conversion.
+ /// Doc values avoid stored-field decompression. Mutable indexes contain
only the stored field; older immutable
+ /// indexes and documents without doc values also use the per-document
stored-field fallback.
+ /// Returns whether the final mapping is the identity mapping.
+ public static boolean fillDocIdMapping(IndexReader indexReader,
PinotDataBuffer buffer, int numDocs,
+ @Nullable int[] mutableToImmutableDocIdMap)
+ throws IOException {
+ NumericDocValues pinotDocIdValues =
+ MultiDocValues.getNumericValues(indexReader,
LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME);
+ if (pinotDocIdValues == null) {
+ // A TEXT column named PinotDocID uses the alternate numeric field to
avoid a field-schema collision.
+ pinotDocIdValues =
+ MultiDocValues.getNumericValues(indexReader,
LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME);
+ }
+ PinotDocIdVisitor visitor = new PinotDocIdVisitor();
+ StoredFields storedFields = null;
+ boolean isIdentityMapping = true;
+ for (int luceneDocId = 0; luceneDocId < numDocs; luceneDocId++) {
+ int pinotDocId;
+ if (pinotDocIdValues != null &&
pinotDocIdValues.advanceExact(luceneDocId)) {
+ pinotDocId = Math.toIntExact(pinotDocIdValues.longValue());
+ } else {
+ if (storedFields == null) {
+ storedFields = indexReader.storedFields();
+ }
+ visitor._pinotDocId = Integer.MIN_VALUE;
+ storedFields.document(luceneDocId, visitor);
+ pinotDocId = visitor._pinotDocId;
+ if (pinotDocId == Integer.MIN_VALUE) {
+ throw new IOException("Missing Pinot document id for Lucene
document: " + luceneDocId);
+ }
+ }
+ if (mutableToImmutableDocIdMap != null) {
+ // Conversion can reorder Pinot rows independently of Lucene's own
document order.
+ pinotDocId = mutableToImmutableDocIdMap[pinotDocId];
+ }
+ isIdentityMapping &= luceneDocId == pinotDocId;
+ buffer.putInt((long) luceneDocId * Integer.BYTES, pinotDocId);
+ }
+ return isIdentityMapping;
+ }
+
+ /// Visits only the stored Pinot document id. Each mapping operation owns
its visitor.
+ private static class PinotDocIdVisitor extends StoredFieldVisitor {
+ private int _pinotDocId;
+
+ @Override
+ public Status needsField(FieldInfo fieldInfo) {
+ return
LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME.equals(fieldInfo.name) ?
Status.YES : Status.NO;
+ }
+
+ @Override
+ public void intField(FieldInfo fieldInfo, int value) {
+ _pinotDocId = value;
+ }
+
+ @Override
+ public void stringField(FieldInfo fieldInfo, String value) {
+ _pinotDocId = Integer.parseInt(value);
+ }
+ }
+
public static void cleanupTextIndex(File segDir, String column) {
// Remove the lucene index file and potentially the docId mapping file.
File luceneIndexFile = new File(segDir, column +
Indexes.LUCENE_TEXT_INDEX_FILE_EXTENSION);
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/LuceneTextIndexCreatorTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/LuceneTextIndexCreatorTest.java
index 04dbb8a71a9..e6b2cc754ee 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/LuceneTextIndexCreatorTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/LuceneTextIndexCreatorTest.java
@@ -20,6 +20,7 @@ package org.apache.pinot.segment.local.segment.index.creator;
import java.io.File;
import java.io.IOException;
+import java.nio.ByteOrder;
import java.nio.file.Files;
import java.util.HashMap;
import org.apache.commons.io.FileUtils;
@@ -31,18 +32,33 @@ import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.MultiDocValues;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import
org.apache.pinot.segment.local.segment.creator.impl.text.LuceneTextIndexCreator;
+import
org.apache.pinot.segment.local.segment.index.readers.text.LuceneTextIndexBufferReader;
import
org.apache.pinot.segment.local.segment.index.readers.text.LuceneTextIndexReader;
import
org.apache.pinot.segment.local.segment.index.text.TextIndexConfigBuilder;
+import org.apache.pinot.segment.local.segment.store.TextIndexUtils;
import org.apache.pinot.segment.spi.V1Constants;
import org.apache.pinot.segment.spi.index.TextIndexConfig;
-import org.testng.Assert;
+import org.apache.pinot.segment.spi.index.TextIndexConfig.DocIdTranslatorMode;
+import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
+import org.apache.pinot.spi.config.table.FieldConfig;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
+
public class LuceneTextIndexCreatorTest {
private static final File INDEX_DIR =
@@ -77,9 +93,9 @@ public class LuceneTextIndexCreatorTest {
// Use TextIndex reader to validate that reads work
try (LuceneTextIndexReader reader = new LuceneTextIndexReader("foo",
INDEX_DIR, 5, new HashMap<>())) {
int[] matchedDocIds = reader.getDocIds("clean").toArray();
- Assert.assertEquals(matchedDocIds.length, 2);
- Assert.assertEquals(matchedDocIds[0], 0);
- Assert.assertEquals(matchedDocIds[1], 4);
+ assertEquals(matchedDocIds.length, 2);
+ assertEquals(matchedDocIds[0], 0);
+ assertEquals(matchedDocIds[1], 4);
}
}
@@ -89,8 +105,8 @@ public class LuceneTextIndexCreatorTest {
// Use TextIndex reader to validate that reads work
try (LuceneTextIndexReader reader = new LuceneTextIndexReader("foo",
INDEX_DIR, 5, new HashMap<>())) {
int[] matchedDocIds = reader.getDocIds("hold").toArray();
- Assert.assertEquals(matchedDocIds.length, 1);
- Assert.assertEquals(matchedDocIds[0], 3);
+ assertEquals(matchedDocIds.length, 1);
+ assertEquals(matchedDocIds[0], 3);
}
}
@@ -100,8 +116,8 @@ public class LuceneTextIndexCreatorTest {
// Use TextIndex reader to validate that reads work
try (LuceneTextIndexReader reader = new LuceneTextIndexReader("foo",
INDEX_DIR, 5, new HashMap<>())) {
int[] matchedDocIds = reader.getDocIds("retain").toArray();
- Assert.assertEquals(matchedDocIds.length, 1);
- Assert.assertEquals(matchedDocIds[0], 1);
+ assertEquals(matchedDocIds.length, 1);
+ assertEquals(matchedDocIds[0], 1);
}
}
@@ -111,16 +127,185 @@ public class LuceneTextIndexCreatorTest {
// Use TextIndex reader to validate that reads work
try (LuceneTextIndexReader reader = new LuceneTextIndexReader("foo",
INDEX_DIR, 5, new HashMap<>())) {
int[] matchedDocIds = reader.getDocIds("retain|keep").toArray();
- Assert.assertEquals(matchedDocIds.length, 2);
- Assert.assertEquals(matchedDocIds[0], 1);
- Assert.assertEquals(matchedDocIds[1], 2);
+ assertEquals(matchedDocIds.length, 2);
+ assertEquals(matchedDocIds[0], 1);
+ assertEquals(matchedDocIds[1], 2);
}
}
- /// Regression test for the AIOOB triggered when a prior segment-conversion
attempt left stale
- /// Lucene files at the v1 destination directory and the reuse-mutable-index
path is taken.
@Test
- public void testConvertMutableSegmentCleansStaleDestFiles()
+ public void testDocValuesReplaceStoredIdsForSingleAndMultiValueDocuments()
+ throws IOException {
+ String column = "values";
+ TextIndexConfig config = new TextIndexConfigBuilder().build();
+ try (LuceneTextIndexCreator creator =
+ new LuceneTextIndexCreator(column, INDEX_DIR, true, false, null, null,
config)) {
+ creator.add("");
+ creator.add(new String[]{"alpha", "beta"}, 2);
+ creator.add(new String[0], 0);
+ creator.add("gamma");
+ creator.seal();
+ }
+
+ File indexDir = new File(INDEX_DIR, column +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
+ try (Directory directory = FSDirectory.open(indexDir.toPath());
+ DirectoryReader reader = DirectoryReader.open(directory)) {
+ NumericDocValues ids =
+ MultiDocValues.getNumericValues(reader,
LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME);
+ assertNull(MultiDocValues.getNumericValues(reader,
LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME));
+ assertEquals(reader.maxDoc(), 4);
+ for (int i = 0; i < reader.maxDoc(); i++) {
+ assertTrue(ids.advanceExact(i));
+ assertEquals(ids.longValue(), i);
+
assertNull(reader.storedFields().document(i).get(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME));
+ }
+ }
+ try (LuceneTextIndexReader reader = new LuceneTextIndexReader(column,
INDEX_DIR, 4, config)) {
+ assertEquals(reader.getDocIds("alpha", null).toArray(), new int[]{1});
+ assertEquals(reader.getDocIds("gamma", null).toArray(), new int[]{3});
+ assertEquals(reader.getDocIds("*:* ", null).toArray(), new int[]{0, 1,
2, 3});
+ }
+ }
+
+ @DataProvider
+ public Object[][] persistedMappingModes() {
+ return new Object[][]{
+ {false, true, null}, {true, true, DocIdTranslatorMode.Default},
+ {false, false, DocIdTranslatorMode.Default}, {true, false,
DocIdTranslatorMode.TryOptimize},
+ {false, true, DocIdTranslatorMode.Skip}, {true, true,
DocIdTranslatorMode.Skip}
+ };
+ }
+
+ @Test(dataProvider = "persistedMappingModes")
+ public void testPersistedIndexMapping(boolean combined, boolean identity,
DocIdTranslatorMode translatorMode)
+ throws Exception {
+ String column = "mapping";
+ TextIndexConfig config = new
TextIndexConfigBuilder().setStoreInSegmentFile(combined)
+ .withDocIdTranslatorMode(translatorMode == null ? null :
translatorMode.name()).build();
+ int[] pinotDocIds = identity ? new int[]{0, 1, 2} : new int[]{2, 0, 1};
+ try (LuceneTextIndexCreator creator =
+ new LuceneTextIndexCreator(column, INDEX_DIR, true, false, null,
false, null, combined, config)) {
+ creator.add("alpha");
+ creator.add("beta");
+ creator.add("gamma");
+ if (!identity) {
+ String[] terms = {"alpha", "beta", "gamma"};
+ for (int i = 0; i < terms.length; i++) {
+ creator.getIndexWriter().updateNumericDocValue(new Term(column,
terms[i]),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME,
pinotDocIds[i]);
+ }
+ }
+ creator.seal();
+ try (DirectoryReader reader =
DirectoryReader.open(creator.getIndexWriter())) {
+ NumericDocValues ids =
+ MultiDocValues.getNumericValues(reader,
LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME);
+ if (translatorMode == DocIdTranslatorMode.Skip) {
+ assertNull(ids);
+ } else {
+ assertNotNull(ids);
+ }
+ for (int i = 0; i < pinotDocIds.length; i++) {
+
assertNull(reader.storedFields().document(i).get(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME));
+ if (ids != null) {
+ assertTrue(ids.advanceExact(i));
+ assertEquals(ids.longValue(), pinotDocIds[i]);
+ }
+ }
+ }
+ }
+
+ File indexDir = new File(INDEX_DIR, column +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
+ File combinedFile = new File(INDEX_DIR, column +
V1Constants.Indexes.LUCENE_COMBINE_TEXT_INDEX_FILE_EXTENSION);
+ String mappingFileName = column +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION;
+ try (PinotDataBuffer buffer = combined
+ ? PinotDataBuffer.mapFile(combinedFile, true, 0,
combinedFile.length(), ByteOrder.LITTLE_ENDIAN,
+ "combined text index test")
+ : null) {
+ assertEquals(combined
+ ? LuceneTextIndexBufferReader.hasFile(buffer, mappingFileName)
+ : new File(INDEX_DIR, mappingFileName).exists(), !identity);
+ DocIdTranslatorMode persistedMode = identity ? DocIdTranslatorMode.Skip
: DocIdTranslatorMode.Default;
+ if (combined) {
+ assertEquals(LuceneTextIndexBufferReader.extractProperties(buffer,
column)
+
.getProperty(FieldConfig.TEXT_INDEX_LUCENE_DOC_ID_TRANSLATOR_MODE),
persistedMode.name());
+ } else {
+ assertEquals(TextIndexUtils.getUpdatedConfigFromPropertiesFile(
+ new File(indexDir,
V1Constants.Indexes.LUCENE_TEXT_INDEX_PROPERTIES_FILE),
config).getDocIdTranslatorMode(),
+ persistedMode);
+ }
+ try (LuceneTextIndexReader reader = combined
+ ? new LuceneTextIndexReader(column, buffer, 3, config)
+ : new LuceneTextIndexReader(column, INDEX_DIR, 3, config)) {
+ assertEquals(reader.getDocIds("alpha", null).toArray(), new
int[]{pinotDocIds[0]});
+ assertEquals(reader.getDocIds("gamma", null).toArray(), new
int[]{pinotDocIds[2]});
+ assertEquals(reader.getDocIds("*:*", null).toArray(), new int[]{0, 1,
2});
+ }
+ }
+ }
+
+ @Test
+ public void testDocValuesFieldNameColumnWithEmptyValues()
+ throws IOException {
+ String column = LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME;
+ TextIndexConfig config = new TextIndexConfigBuilder().build();
+ try (LuceneTextIndexCreator creator =
+ new LuceneTextIndexCreator(column, INDEX_DIR, true, false, null, null,
config)) {
+ creator.add(new String[0], 0);
+ creator.add(new String[]{"alpha"}, 1);
+ creator.seal();
+ try (DirectoryReader reader =
DirectoryReader.open(creator.getIndexWriter())) {
+ NumericDocValues ids =
+ MultiDocValues.getNumericValues(reader,
LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME);
+ for (int i = 0; i < 2; i++) {
+ assertTrue(ids.advanceExact(i));
+ assertEquals(ids.longValue(), i);
+
assertNull(reader.storedFields().document(i).get(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME));
+ }
+ }
+ }
+ try (LuceneTextIndexReader reader = new LuceneTextIndexReader(column,
INDEX_DIR, 2, config)) {
+ assertEquals(reader.getDocIds("alpha", null).toArray(), new int[]{1});
+ assertEquals(reader.getDocIds("*:* ", null).toArray(), new int[]{0, 1});
+ }
+ }
+
+ @Test
+ public void testMutableIndexRetainsStoredFieldFallback()
+ throws IOException {
+ String column = "mutableValues";
+ TextIndexConfig config = new TextIndexConfigBuilder().build();
+ try (LuceneTextIndexCreator creator =
+ new LuceneTextIndexCreator(column, INDEX_DIR, false, false, null,
null, config)) {
+ creator.add("alpha");
+ creator.add(new String[0], 0);
+ creator.getIndexWriter().commit();
+ try (DirectoryReader reader =
DirectoryReader.open(creator.getIndexWriter())) {
+ assertNull(MultiDocValues.getNumericValues(reader,
LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME));
+ assertEquals(reader.storedFields().document(0)
+ .get(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME), "0");
+ }
+ try (LuceneTextIndexReader reader = new LuceneTextIndexReader(column,
INDEX_DIR, 2, config)) {
+ assertEquals(reader.getDocIds("alpha", null).toArray(), new int[]{0});
+ assertEquals(reader.getDocIds("*:* ", null).toArray(), new int[]{0,
1});
+ }
+ }
+ }
+
+ @DataProvider
+ public Object[][] mutableMappingModes() {
+ return new Object[][]{
+ {null, new int[]{0, 1, 2}, null, true},
+ {DocIdTranslatorMode.TryOptimize, new int[]{0, 1, 2}, null, false},
+ {DocIdTranslatorMode.TryOptimize, new int[]{0, 1, 2}, new int[]{2, 0,
1}, true},
+ // Only the final immutable IDs are sequential in this case.
+ {DocIdTranslatorMode.TryOptimize, new int[]{2, 0, 1}, new int[]{1, 2,
0}, false}
+ };
+ }
+
+ /// Regression test for stale destination files, including mappings made
identity or nonidentity by conversion.
+ @Test(dataProvider = "mutableMappingModes")
+ public void
testConvertMutableSegmentCleansStaleDestFiles(DocIdTranslatorMode
translatorMode, int[] mutableDocIds,
+ int[] mutableToImmutableDocIdMap, boolean mappingExpected)
throws Exception {
String column = "foo";
String segmentName = "regSeg";
@@ -135,7 +320,12 @@ public class LuceneTextIndexCreatorTest {
File mutableIndexDir = new File(new File(consumerDir, segmentName),
column + V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
FileUtils.forceMkdir(mutableIndexDir);
- writeLuceneDocs(mutableIndexDir, /* startDocId */ 0, numMutableDocs, /*
multiSegment */ false);
+ try (Directory directory = FSDirectory.open(mutableIndexDir.toPath());
+ IndexWriter writer = new IndexWriter(directory, new
IndexWriterConfig(new StandardAnalyzer()))) {
+ for (int mutableDocId : mutableDocIds) {
+ writer.addDocument(buildDoc(mutableDocId));
+ }
+ }
// 2. segmentIndexDir's parent
File tmpSegmentParent = new File(scratch, "tmp-" + segmentName + "-1");
@@ -154,16 +344,17 @@ public class LuceneTextIndexCreatorTest {
// 4. commit=true && realtimeConversion=true && reuseMutableIndex=true
triggers
// convertMutableSegment.
- TextIndexConfig config = new
TextIndexConfigBuilder().withReuseMutableIndex(true).build();
+ TextIndexConfig config = new
TextIndexConfigBuilder().withReuseMutableIndex(true)
+ .withDocIdTranslatorMode(translatorMode == null ? null :
translatorMode.name()).build();
try (LuceneTextIndexCreator creator = new LuceneTextIndexCreator(column,
segmentIndexDir,
/* commit */ true, /* realtimeConversion */ true, consumerDir,
- /* mutableSegmentCompacted */ false, /* immutableToMutableIdMap */
null,
+ /* mutableSegmentCompacted */ false, mutableToImmutableDocIdMap,
/* combineAndCleanupFiles */ false, config)) {
// Constructor performs the conversion; no add()/seal() on the reuse
path.
}
// 5a. Dest wipe: sentinel must be gone.
- Assert.assertFalse(staleSentinel.exists(),
+ assertFalse(staleSentinel.exists(),
"Stale file at v1 destination must be removed before conversion to "
+ "avoid CREATE_OR_APPEND picking up leftovers from a prior
attempt");
@@ -172,7 +363,7 @@ public class LuceneTextIndexCreatorTest {
// numStaleDocs instead.
try (Directory destDir = FSDirectory.open(destIndexDir.toPath());
DirectoryReader reader = DirectoryReader.open(destDir)) {
- Assert.assertEquals(reader.numDocs(), numMutableDocs,
+ assertEquals(reader.numDocs(), numMutableDocs,
"Converted Lucene index must contain exactly the mutable input's
document count, not "
+ "stale leftovers from a prior attempt");
}
@@ -181,24 +372,64 @@ public class LuceneTextIndexCreatorTest {
// segment's numDocs at read time - a mismatch here is what produced
AIOOB in production.
File mappingFile = new File(segmentIndexDir,
column +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
- Assert.assertTrue(mappingFile.exists(), "DocId mapping file should be
built during conversion");
- Assert.assertEquals(mappingFile.length(), (long) Integer.BYTES *
numMutableDocs,
- "DocId mapping file must be sized for the segment's numDocs only");
+ assertEquals(mappingFile.exists(), mappingExpected);
+ if (mappingExpected) {
+ assertEquals(mappingFile.length(), (long) Integer.BYTES *
numMutableDocs,
+ "DocId mapping file must be sized for the segment's numDocs only");
+ }
+ if (translatorMode == DocIdTranslatorMode.TryOptimize) {
+ File propertiesFile = new File(destIndexDir,
V1Constants.Indexes.LUCENE_TEXT_INDEX_PROPERTIES_FILE);
+
assertEquals(TextIndexUtils.getUpdatedConfigFromPropertiesFile(propertiesFile,
config).getDocIdTranslatorMode(),
+ mappingExpected ? DocIdTranslatorMode.Default :
DocIdTranslatorMode.Skip);
+ }
// 5d. End-to-end read
try (LuceneTextIndexReader reader = new LuceneTextIndexReader(column,
segmentIndexDir,
numMutableDocs, config)) {
- int[] matched = reader.getDocIds("doc").toArray();
- for (int docId : matched) {
- Assert.assertTrue(docId >= 0 && docId < numMutableDocs,
- "Returned Pinot docId " + docId + " must be within [0, " +
numMutableDocs + ")");
- }
+ assertEquals(reader.getDocIds("doc", null).toArray(), new int[]{0, 1,
2});
+ assertEquals(reader.getDocIds("0", null).toArray(),
+ new int[]{mutableToImmutableDocIdMap == null ? 0 :
mutableToImmutableDocIdMap[0]});
}
} finally {
FileUtils.deleteDirectory(scratch);
}
}
+ @Test
+ public void testConvertMutableSegmentRejectsMissingStoredDocId()
+ throws IOException {
+ String column = "foo";
+ String segmentName = "missingDocId";
+ File consumerDir = new File(INDEX_DIR, "consumerDir");
+ File mutableIndexDir = new File(new File(consumerDir, segmentName),
+ column + V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
+ FileUtils.forceMkdir(mutableIndexDir);
+ try (Directory directory = FSDirectory.open(mutableIndexDir.toPath());
+ IndexWriter writer = new IndexWriter(directory, new
IndexWriterConfig(new StandardAnalyzer()))) {
+ writer.addDocument(buildDoc(0));
+ Document corruptDocument = buildDoc(1);
+
corruptDocument.removeField(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME);
+ writer.addDocument(corruptDocument);
+ writer.commit();
+ }
+
+ File segmentIndexDir = new File(new File(INDEX_DIR, "tmp-" + segmentName +
"-1"), segmentName);
+ FileUtils.forceMkdir(segmentIndexDir);
+ TextIndexConfig config = new
TextIndexConfigBuilder().withReuseMutableIndex(true).build();
+ RuntimeException exception = expectThrows(RuntimeException.class, () -> {
+ try (LuceneTextIndexCreator creator =
+ new LuceneTextIndexCreator(column, segmentIndexDir, true, true,
consumerDir, null, config)) {
+ // Conversion must reject the legacy index before a reader can consume
its mapping.
+ }
+ });
+ Throwable rootCause = exception;
+ while (rootCause.getCause() != null) {
+ rootCause = rootCause.getCause();
+ }
+ assertTrue(rootCause instanceof IOException);
+ assertEquals(rootCause.getMessage(), "Missing Pinot document id for Lucene
document: 1");
+ }
+
private static void writeLuceneDocs(File indexDir, int startDocId, int
count, boolean multiSegment)
throws IOException {
try (Directory directory = FSDirectory.open(indexDir.toPath());
@@ -224,7 +455,7 @@ public class LuceneTextIndexCreatorTest {
private static Document buildDoc(int docId) {
Document doc = new Document();
doc.add(new TextField("foo", "doc " + docId, Field.Store.NO));
- doc.add(new
StoredField(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME, docId));
+ doc.add(new
StoredField(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME, docId));
return doc;
}
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java
index c61d4ad7fc6..ad08ef1f9e4 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/LoaderTest.java
@@ -28,7 +28,9 @@ import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoa
import org.apache.pinot.segment.local.segment.creator.SegmentTestUtils;
import
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
import
org.apache.pinot.segment.local.segment.index.converter.SegmentV1V2ToV3FormatConverter;
+import
org.apache.pinot.segment.local.segment.index.text.TextIndexConfigBuilder;
import org.apache.pinot.segment.local.segment.store.SegmentLocalFSDirectory;
+import org.apache.pinot.segment.local.segment.store.TextIndexUtils;
import org.apache.pinot.segment.local.utils.SegmentOperationsThrottler;
import org.apache.pinot.segment.local.utils.SegmentOperationsThrottlerSet;
import org.apache.pinot.segment.spi.ImmutableSegment;
@@ -39,6 +41,7 @@ import
org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
import org.apache.pinot.segment.spi.creator.SegmentVersion;
import org.apache.pinot.segment.spi.datasource.DataSource;
import org.apache.pinot.segment.spi.index.StandardIndexes;
+import org.apache.pinot.segment.spi.index.TextIndexConfig.DocIdTranslatorMode;
import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
import org.apache.pinot.segment.spi.store.SegmentDirectory;
@@ -670,19 +673,12 @@ public class LoaderTest {
verifyIndexDirIsV3(_indexDir);
// no change/conversion should have happened for textIndex dir
textIndexFile = SegmentDirectoryPaths.findTextIndexIndexFile(_indexDir,
TEXT_INDEX_COL_NAME);
- // segment load should have created the docID mapping file in V3 structure
- File textIndexDocIdMappingFile =
- SegmentDirectoryPaths.findTextIndexDocIdMappingFile(_indexDir,
TEXT_INDEX_COL_NAME);
assertNotNull(textIndexFile);
- assertNotNull(textIndexDocIdMappingFile);
assertTrue(textIndexFile.isDirectory());
- assertFalse(textIndexDocIdMappingFile.isDirectory());
assertEquals(textIndexFile.getName(),
TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
assertEquals(textIndexFile.getParentFile().getName(),
SegmentDirectoryPaths.V3_SUBDIRECTORY_NAME);
- assertEquals(textIndexDocIdMappingFile.getName(),
- TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
- assertEquals(textIndexDocIdMappingFile.getParentFile().getName(),
SegmentDirectoryPaths.V3_SUBDIRECTORY_NAME);
+ assertIdentityTextIndexMapping(indexSegment, textIndexFile);
indexSegment.destroy();
// CASE 2: set the segment version to load in IndexLoadingConfig as V3
@@ -699,18 +695,12 @@ public class LoaderTest {
verifyIndexDirIsV3(_indexDir);
// no change/conversion should have happened for textIndex dir
textIndexFile = SegmentDirectoryPaths.findTextIndexIndexFile(_indexDir,
TEXT_INDEX_COL_NAME);
- // segment load should have created the docID mapping file in V3 structure
- textIndexDocIdMappingFile =
SegmentDirectoryPaths.findTextIndexDocIdMappingFile(_indexDir,
TEXT_INDEX_COL_NAME);
assertNotNull(textIndexFile);
- assertNotNull(textIndexDocIdMappingFile);
assertTrue(textIndexFile.isDirectory());
- assertFalse(textIndexDocIdMappingFile.isDirectory());
assertEquals(textIndexFile.getName(),
TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
assertEquals(textIndexFile.getParentFile().getName(),
SegmentDirectoryPaths.V3_SUBDIRECTORY_NAME);
- assertEquals(textIndexDocIdMappingFile.getName(),
- TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
- assertEquals(textIndexDocIdMappingFile.getParentFile().getName(),
SegmentDirectoryPaths.V3_SUBDIRECTORY_NAME);
+ assertIdentityTextIndexMapping(indexSegment, textIndexFile);
indexSegment.destroy();
// Test for scenarios by creating on-disk segment in V1 and then loading
@@ -730,7 +720,6 @@ public class LoaderTest {
textIndexFile = SegmentDirectoryPaths.findTextIndexIndexFile(_indexDir,
TEXT_INDEX_COL_NAME);
assertNotNull(textIndexFile);
assertTrue(textIndexFile.isDirectory());
- assertFalse(textIndexDocIdMappingFile.isDirectory());
assertEquals(textIndexFile.getName(),
TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
assertEquals(textIndexFile.getParentFile().getName(), new
SegmentMetadataImpl(_indexDir).getName());
@@ -748,17 +737,12 @@ public class LoaderTest {
assertFalse(SegmentDirectoryPaths.segmentDirectoryFor(_indexDir,
SegmentVersion.v3).exists());
// no change/conversion should have happened in text index Dir
textIndexFile = SegmentDirectoryPaths.findTextIndexIndexFile(_indexDir,
TEXT_INDEX_COL_NAME);
- // segment load should have created the docID mapping file in V1 structure
- textIndexDocIdMappingFile =
SegmentDirectoryPaths.findTextIndexDocIdMappingFile(_indexDir,
TEXT_INDEX_COL_NAME);
assertNotNull(textIndexFile);
- assertNotNull(textIndexDocIdMappingFile);
assertTrue(textIndexFile.isDirectory());
assertEquals(textIndexFile.getName(),
TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
assertEquals(textIndexFile.getParentFile().getName(), new
SegmentMetadataImpl(_indexDir).getName());
- assertEquals(textIndexDocIdMappingFile.getName(),
- TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
- assertEquals(textIndexDocIdMappingFile.getParentFile().getName(), new
SegmentMetadataImpl(_indexDir).getName());
+ assertIdentityTextIndexMapping(indexSegment, textIndexFile);
indexSegment.destroy();
// CASE 2: set the segment version to load in IndexLoadingConfig to V1
@@ -774,17 +758,12 @@ public class LoaderTest {
assertFalse(SegmentDirectoryPaths.segmentDirectoryFor(_indexDir,
SegmentVersion.v3).exists());
// no change/conversion should have happened in text index Dir
textIndexFile = SegmentDirectoryPaths.findTextIndexIndexFile(_indexDir,
TEXT_INDEX_COL_NAME);
- // segment load should have created the docID mapping file in V1 structure
- textIndexDocIdMappingFile =
SegmentDirectoryPaths.findTextIndexDocIdMappingFile(_indexDir,
TEXT_INDEX_COL_NAME);
assertNotNull(textIndexFile);
- assertNotNull(textIndexDocIdMappingFile);
assertTrue(textIndexFile.isDirectory());
assertEquals(textIndexFile.getName(),
TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
assertEquals(textIndexFile.getParentFile().getName(), new
SegmentMetadataImpl(_indexDir).getName());
- assertEquals(textIndexDocIdMappingFile.getName(),
- TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
- assertEquals(textIndexDocIdMappingFile.getParentFile().getName(), new
SegmentMetadataImpl(_indexDir).getName());
+ assertIdentityTextIndexMapping(indexSegment, textIndexFile);
indexSegment.destroy();
// CASE 3: set the segment version to load in IndexLoadingConfig to V3
@@ -800,20 +779,27 @@ public class LoaderTest {
verifyIndexDirIsV3(_indexDir);
// check that text index exists under V3 subdir. It should exist and
should be a subdir
textIndexFile = SegmentDirectoryPaths.findTextIndexIndexFile(_indexDir,
TEXT_INDEX_COL_NAME);
- // segment load should have created the docID mapping file in V3 structure
- textIndexDocIdMappingFile =
SegmentDirectoryPaths.findTextIndexDocIdMappingFile(_indexDir,
TEXT_INDEX_COL_NAME);
assertNotNull(textIndexFile);
- assertNotNull(textIndexDocIdMappingFile);
assertTrue(textIndexFile.isDirectory());
assertEquals(textIndexFile.getName(),
TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
assertEquals(textIndexFile.getParentFile().getName(),
SegmentDirectoryPaths.V3_SUBDIRECTORY_NAME);
- assertEquals(textIndexDocIdMappingFile.getName(),
- TEXT_INDEX_COL_NAME +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION);
- assertEquals(textIndexDocIdMappingFile.getParentFile().getName(),
SegmentDirectoryPaths.V3_SUBDIRECTORY_NAME);
+ assertIdentityTextIndexMapping(indexSegment, textIndexFile);
indexSegment.destroy();
}
+ private void assertIdentityTextIndexMapping(IndexSegment indexSegment, File
textIndexDir)
+ throws Exception {
+ // Verified identity mappings remain Skip across loads and V1-to-V3
conversion, without a sidecar.
+ assertNull(SegmentDirectoryPaths.findTextIndexDocIdMappingFile(_indexDir,
TEXT_INDEX_COL_NAME));
+ File propertiesFile = new File(textIndexDir,
V1Constants.Indexes.LUCENE_TEXT_INDEX_PROPERTIES_FILE);
+
assertEquals(TextIndexUtils.getUpdatedConfigFromPropertiesFile(propertiesFile,
+ new TextIndexConfigBuilder().build())
+ .getDocIdTranslatorMode(), DocIdTranslatorMode.Skip);
+
assertEquals(indexSegment.getDataSource(TEXT_INDEX_COL_NAME).getTextIndex().getDocIds("*:*",
null).getCardinality(),
+ indexSegment.getSegmentMetadata().getTotalDocs());
+ }
+
private void constructSegmentWithVectorIndex(SegmentVersion segmentVersion)
throws Exception {
FileUtils.deleteQuietly(INDEX_DIR);
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/invertedindex/TextIndexRollbackRecoveryTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/invertedindex/TextIndexRollbackRecoveryTest.java
index 453278715b5..88438678440 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/invertedindex/TextIndexRollbackRecoveryTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/invertedindex/TextIndexRollbackRecoveryTest.java
@@ -356,7 +356,7 @@ public class TextIndexRollbackRecoveryTest implements
PinotBuffersAfterClassChec
new
IndexWriterConfig(analyzer).setOpenMode(IndexWriterConfig.OpenMode.CREATE))) {
Document document = new Document();
document.add(new TextField("text", "partial", Field.Store.NO));
- document.add(new
StoredField(LuceneTextIndexCreator.LUCENE_INDEX_DOC_ID_COLUMN_NAME, 0));
+ document.add(new
StoredField(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME, 0));
writer.addDocument(document);
}
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexCompatibleTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexCompatibleTest.java
index 89bdc74a460..4b41fd6439c 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexCompatibleTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexCompatibleTest.java
@@ -20,12 +20,50 @@ package
org.apache.pinot.segment.local.segment.index.readers.text;
import java.io.File;
import java.io.IOException;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
import java.util.Map;
-import org.testng.Assert;
+import org.apache.commons.io.FileUtils;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.NoMergePolicy;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import
org.apache.pinot.segment.local.segment.creator.impl.text.LuceneTextIndexCombined;
+import
org.apache.pinot.segment.local.segment.creator.impl.text.LuceneTextIndexCreator;
+import
org.apache.pinot.segment.local.segment.index.text.TextIndexConfigBuilder;
+import org.apache.pinot.segment.local.segment.store.TextIndexUtils;
+import org.apache.pinot.segment.spi.V1Constants;
+import org.apache.pinot.segment.spi.index.TextIndexConfig;
+import org.apache.pinot.segment.spi.index.TextIndexConfig.DocIdTranslatorMode;
+import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
public class LuceneTextIndexCompatibleTest {
+ private static final String COLUMN = "Text";
+ private static final int[] PINOT_IDS = {2, 0, 3, 1};
@Test
public void testLucene80IndexReader()
@@ -33,7 +71,139 @@ public class LuceneTextIndexCompatibleTest {
File indexPath =
new
File(LuceneTextIndexCompatibleTest.class.getClassLoader().getResource("data/lucene_80_index").getPath());
try (LuceneTextIndexReader lucene80Index = new
LuceneTextIndexReader("Text", indexPath, 1000, Map.of())) {
- Assert.assertNotNull(lucene80Index);
+ assertNotNull(lucene80Index);
+ }
+ }
+
+ @Test
+ public void testDocValuesMappingDoesNotReadStoredDocuments()
+ throws IOException {
+ File segmentDir =
Files.createTempDirectory("lucene-doc-values-mapping").toFile();
+ try {
+ File indexDir = writeMappedIndex(segmentDir, "all", false, false);
+ try (Directory directory = FSDirectory.open(indexDir.toPath());
+ DirectoryReader reader = spy(DirectoryReader.open(directory));
+ PinotDataBuffer buffer =
PinotDataBuffer.allocateDirect(PINOT_IDS.length * Integer.BYTES,
+ ByteOrder.LITTLE_ENDIAN, "doc-id mapping test")) {
+ assertTrue(reader.leaves().size() > 1);
+ assertFalse(TextIndexUtils.fillDocIdMapping(reader, buffer,
PINOT_IDS.length, null));
+ for (int i = 0; i < PINOT_IDS.length; i++) {
+ assertEquals(buffer.getInt(i * Integer.BYTES), PINOT_IDS[i]);
+ }
+ // Remapping the permuted mutable IDs back to Lucene order creates an
identity mapping.
+ assertTrue(TextIndexUtils.fillDocIdMapping(reader, buffer,
PINOT_IDS.length, new int[]{1, 3, 0, 2}));
+ for (int i = 0; i < PINOT_IDS.length; i++) {
+ assertEquals(buffer.getInt(i * Integer.BYTES), i);
+ }
+ verify(reader, never()).storedFields();
+ }
+ } finally {
+ FileUtils.deleteDirectory(segmentDir);
+ }
+ }
+
+ @DataProvider(name = "mappingFormats")
+ public Object[][] mappingFormats() {
+ return new Object[][]{
+ {"none", false, DocIdTranslatorMode.Default, false},
+ {"mixed", false, DocIdTranslatorMode.Default, false},
+ {"all", false, DocIdTranslatorMode.Default, false},
+ {"none", true, DocIdTranslatorMode.TryOptimize, false},
+ {"mixed", true, DocIdTranslatorMode.TryOptimize, false},
+ {"all", true, DocIdTranslatorMode.TryOptimize, false},
+ {"all", false, DocIdTranslatorMode.TryOptimize, true}
+ };
+ }
+
+ @Test(dataProvider = "mappingFormats")
+ public void testLegacyAndMixedDocValuesMapping(String docValuesMode, boolean
deleteDocument,
+ DocIdTranslatorMode translatorMode, boolean sortIndex)
+ throws IOException {
+ File segmentDir =
Files.createTempDirectory("lucene-mixed-doc-values").toFile();
+ try {
+ writeMappedIndex(segmentDir, docValuesMode, deleteDocument, sortIndex);
+ TextIndexConfig config =
+ new
TextIndexConfigBuilder().withDocIdTranslatorMode(translatorMode.name()).build();
+ // Document ids are permuted and span multiple leaves. A deletion must
not shift the remaining mappings.
+ try (LuceneTextIndexReader reader = new LuceneTextIndexReader(COLUMN,
segmentDir, PINOT_IDS.length, config)) {
+ assertEquals(reader.getDocIds("tokenzero", null).toArray(), new
int[]{2});
+ assertEquals(reader.getDocIds("tokentwo", null).toArray(), new
int[]{3});
+ assertEquals(reader.getDocIds("tokenthree", null).toArray(), new
int[]{1});
+ assertEquals(reader.getDocIds("matchall", null).toArray(),
+ deleteDocument ? new int[]{1, 2, 3} : new int[]{0, 1, 2, 3});
+ }
+ // A reload uses the persisted mapping generated from whichever
representation each leaf provided.
+ try (LuceneTextIndexReader reader = new LuceneTextIndexReader(COLUMN,
segmentDir, PINOT_IDS.length, config)) {
+ assertEquals(reader.getDocIds("matchall", null).toArray(),
+ deleteDocument ? new int[]{1, 2, 3} : new int[]{0, 1, 2, 3});
+ }
+ } finally {
+ FileUtils.deleteDirectory(segmentDir);
+ }
+ }
+
+ @Test
+ public void testLegacyCombinedIndexWithoutMappingBuffer()
+ throws IOException {
+ File segmentDir =
Files.createTempDirectory("lucene-legacy-combined").toFile();
+ try {
+ File indexDir = writeMappedIndex(segmentDir, "none", false, false);
+ File combinedFile = new File(segmentDir, COLUMN +
V1Constants.Indexes.LUCENE_COMBINE_TEXT_INDEX_FILE_EXTENSION);
+ LuceneTextIndexCombined.combineLuceneIndexFiles(indexDir,
combinedFile.toString());
+ FileUtils.deleteDirectory(indexDir);
+ TextIndexConfig config = new
TextIndexConfigBuilder().setStoreInSegmentFile(true)
+ .withDocIdTranslatorMode(DocIdTranslatorMode.Default.name()).build();
+ try (PinotDataBuffer buffer = PinotDataBuffer.mapFile(combinedFile,
true, 0, combinedFile.length(),
+ ByteOrder.LITTLE_ENDIAN, "legacy combined text index test")) {
+
assertNull(LuceneTextIndexBufferReader.extractDocIdMappingBuffer(buffer,
COLUMN));
+ try (LuceneTextIndexReader reader = new LuceneTextIndexReader(COLUMN,
buffer, PINOT_IDS.length, config)) {
+ assertEquals(reader.getDocIds("tokenzero", null).toArray(), new
int[]{2});
+ assertEquals(reader.getDocIds("tokentwo", null).toArray(), new
int[]{3});
+ assertEquals(reader.getDocIds("matchall", null).toArray(), new
int[]{0, 1, 2, 3});
+ }
+ }
+ } finally {
+ FileUtils.deleteDirectory(segmentDir);
+ }
+ }
+
+ private static File writeMappedIndex(File segmentDir, String docValuesMode,
boolean deleteDocument, boolean sortIndex)
+ throws IOException {
+ File indexDir = new File(segmentDir, COLUMN +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION);
+ String[] tokens = {"tokenzero", "tokenone", "tokentwo", "tokenthree"};
+ IndexWriterConfig writerConfig = new IndexWriterConfig(new
StandardAnalyzer());
+ if (sortIndex) {
+ writerConfig.setIndexSort(new Sort(new SortField("order",
SortField.Type.INT, true)));
+ } else {
+ writerConfig.setMergePolicy(NoMergePolicy.INSTANCE);
+ }
+ try (Directory directory = FSDirectory.open(indexDir.toPath());
+ IndexWriter writer = new IndexWriter(directory, writerConfig)) {
+ for (int i = 0; i < PINOT_IDS.length; i++) {
+ Document document = new Document();
+ document.add(new TextField(COLUMN, "matchall " + tokens[i],
Field.Store.NO));
+ document.add(new StringField("key", tokens[i], Field.Store.NO));
+ document.add(new
StoredField(LuceneTextIndexCreator.PINOT_DOC_ID_STORED_FIELD_NAME,
PINOT_IDS[i]));
+ if (docValuesMode.equals("all") || docValuesMode.equals("mixed") && i
== 1) {
+ document.add(
+ new
NumericDocValuesField(LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME,
PINOT_IDS[i]));
+ }
+ if (sortIndex) {
+ document.add(new NumericDocValuesField("order", i));
+ }
+ writer.addDocument(document);
+ if (i % 2 == 1) {
+ writer.commit();
+ }
+ }
+ if (deleteDocument) {
+ writer.deleteDocuments(new Term("key", tokens[1]));
+ writer.commit();
+ }
+ if (sortIndex) {
+ writer.forceMerge(1);
+ }
}
+ return indexDir;
}
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexCountTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexCountTest.java
index 92ba615cb22..5f1a8c9489e 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexCountTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/text/LuceneTextIndexCountTest.java
@@ -55,6 +55,7 @@ import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
/// Covers the `count(*)` short-circuit:
[LuceneTextIndexReader#getNumMatchingDocs] and the
@@ -216,23 +217,20 @@ public class LuceneTextIndexCountTest {
}
}
- /// A Lucene index larger than the segment means a broken doc-id mapping.
Counting does not consult the
- /// translator, so it must refuse the fast path and fall back to the doc-id
path, which fails loudly rather
- /// than returning an inflated count.
+ /// Both query paths reject an oversized index even when a persisted Skip
setting removes translator bounds checks.
@Test
- public void testOversizedIndexFallsBackInsteadOfCountingWrong()
+ public void testOversizedIndexRejectsInvalidDocIds()
throws Exception {
int understatedNumDocs = DOCS.length - 4;
try (LuceneTextIndexReader reader =
new LuceneTextIndexReader(COLUMN, INDEX_DIR, understatedNumDocs,
Map.of())) {
- try {
- int count = reader.getNumMatchingDocs("sentinelall", null);
- assertTrue(count <= understatedNumDocs,
- "returned a count larger than the segment (" + count + " > " +
understatedNumDocs + ")");
- } catch (RuntimeException e) {
- // Falling back and failing loudly is the intended behaviour for a
broken mapping. The fallback runs
- // getDocIds, whose translator throws, and getNumMatchingDocs rewraps
it with its own wording.
- assertTrue(e.getMessage().contains("text index"), "unexpected failure:
" + e.getMessage());
+ RuntimeException countFailure =
+ expectThrows(RuntimeException.class, () ->
reader.getNumMatchingDocs("sentinelall", null));
+ RuntimeException idsFailure = expectThrows(RuntimeException.class, () ->
reader.getDocIds("sentinelall", null));
+ for (RuntimeException failure : new RuntimeException[]{countFailure,
idsFailure}) {
+ assertTrue(failure.getMessage().contains("text index"));
+ assertTrue(failure.getCause() instanceof IllegalStateException);
+ assertTrue(failure.getCause().getMessage().contains("exceeding segment
document count"));
}
}
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/store/FilePerIndexDirectoryTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/store/FilePerIndexDirectoryTest.java
index 8254afa938d..a2364555d9b 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/store/FilePerIndexDirectoryTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/store/FilePerIndexDirectoryTest.java
@@ -28,6 +28,7 @@ import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.io.FileUtils;
+import org.apache.lucene.index.Term;
import org.apache.pinot.segment.local.PinotBuffersAfterMethodCheckRule;
import
org.apache.pinot.segment.local.segment.creator.impl.text.LuceneTextIndexCreator;
import
org.apache.pinot.segment.local.segment.index.readers.text.LuceneTextIndexReader;
@@ -204,24 +205,36 @@ public class FilePerIndexDirectoryTest implements
PinotBuffersAfterMethodCheckRu
buf.putChar(111, 'h');
fooCreator.add("{\"clean\":\"this\"}");
+ fooCreator.add("{\"other\":\"that\"}");
+ // Keep valid nonidentity mappings so removal covers both the index and
its required sidecar.
+ fooCreator.getIndexWriter().updateNumericDocValue(new Term("foo",
"clean"),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME, 1);
+ fooCreator.getIndexWriter().updateNumericDocValue(new Term("foo",
"other"),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME, 0);
fooCreator.seal();
barCreator.add("{\"retain\":\"this\"}");
barCreator.add("{\"keep\":\"this\"}");
barCreator.add("{\"hold\":\"this\"}");
+ barCreator.getIndexWriter().updateNumericDocValue(new Term("bar",
"retain"),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME, 2);
+ barCreator.getIndexWriter().updateNumericDocValue(new Term("bar",
"hold"),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME, 0);
barCreator.seal();
}
// Remove the Text index to trigger cleanup.
try (FilePerIndexDirectory fpi = new FilePerIndexDirectory(TEMP_DIR,
_segmentMetadata, ReadMode.mmap)) {
assertTrue(fpi.hasIndexFor("foo", StandardIndexes.text()));
- // Use TextIndex once to trigger the creation of mapping files.
- try (LuceneTextIndexReader fooReader = new LuceneTextIndexReader("foo",
TEMP_DIR, 1, new HashMap<>())) {
- fooReader.getDocIds("clean");
+ // Verify that the persisted mappings translate the permuted IDs before
cleanup.
+ try (LuceneTextIndexReader fooReader = new LuceneTextIndexReader("foo",
TEMP_DIR, 2, new HashMap<>())) {
+ assertEquals(fooReader.getDocIds("clean", null).toArray(), new
int[]{1});
}
try (LuceneTextIndexReader barReader = new LuceneTextIndexReader("bar",
TEMP_DIR, 3, new HashMap<>())) {
- barReader.getDocIds("retain hold");
+ assertEquals(barReader.getDocIds("retain", null).toArray(), new
int[]{2});
}
+ assertTrue(
+ new File(TEMP_DIR, "foo" +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION).exists());
// Both files for TextIndex should be removed.
fpi.removeIndex("foo", StandardIndexes.text());
assertFalse(new File(TEMP_DIR, "foo" +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION).exists());
@@ -248,6 +261,7 @@ public class FilePerIndexDirectoryTest implements
PinotBuffersAfterMethodCheckRu
// Check if the text index still work.
try (LuceneTextIndexReader barReader = new LuceneTextIndexReader("bar",
TEMP_DIR, 3, new HashMap<>())) {
+ assertEquals(barReader.getDocIds("retain", null).toArray(), new
int[]{2});
MutableRoaringBitmap ids = barReader.getDocIds("retain hold");
assertTrue(ids.contains(0));
assertTrue(ids.contains(2));
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/store/SingleFileIndexDirectoryTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/store/SingleFileIndexDirectoryTest.java
index 3825363d5f9..4b620248297 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/store/SingleFileIndexDirectoryTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/store/SingleFileIndexDirectoryTest.java
@@ -34,6 +34,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.io.FileUtils;
+import org.apache.lucene.index.Term;
import org.apache.pinot.segment.local.PinotBuffersAfterMethodCheckRule;
import
org.apache.pinot.segment.local.segment.creator.impl.text.LuceneTextIndexCreator;
import
org.apache.pinot.segment.local.segment.index.readers.text.LuceneTextIndexReader;
@@ -252,24 +253,36 @@ public class SingleFileIndexDirectoryTest implements
PinotBuffersAfterMethodChec
buf.putChar(111, 'h');
fooCreator.add("{\"clean\":\"this\"}");
+ fooCreator.add("{\"other\":\"that\"}");
+ // Keep valid nonidentity mappings so removal covers both the index and
its required sidecar.
+ fooCreator.getIndexWriter().updateNumericDocValue(new Term("foo",
"clean"),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME, 1);
+ fooCreator.getIndexWriter().updateNumericDocValue(new Term("foo",
"other"),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME, 0);
fooCreator.seal();
barCreator.add("{\"retain\":\"this\"}");
barCreator.add("{\"keep\":\"this\"}");
barCreator.add("{\"hold\":\"this\"}");
+ barCreator.getIndexWriter().updateNumericDocValue(new Term("bar",
"retain"),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME, 2);
+ barCreator.getIndexWriter().updateNumericDocValue(new Term("bar",
"hold"),
+ LuceneTextIndexCreator.PINOT_DOC_ID_DOC_VALUES_FIELD_NAME, 0);
barCreator.seal();
}
// Remove the Text index to trigger cleanup.
try (SingleFileIndexDirectory sfd = new SingleFileIndexDirectory(TEMP_DIR,
_segmentMetadata, ReadMode.mmap)) {
assertTrue(sfd.hasIndexFor("foo", StandardIndexes.text()));
- // Use TextIndex once to trigger the creation of mapping files.
- try (LuceneTextIndexReader fooReader = new LuceneTextIndexReader("foo",
TEMP_DIR, 1, new HashMap<>())) {
- fooReader.getDocIds("clean");
+ // Verify that the persisted mappings translate the permuted IDs before
cleanup.
+ try (LuceneTextIndexReader fooReader = new LuceneTextIndexReader("foo",
TEMP_DIR, 2, new HashMap<>())) {
+ assertEquals(fooReader.getDocIds("clean", null).toArray(), new
int[]{1});
}
try (LuceneTextIndexReader barReader = new LuceneTextIndexReader("bar",
TEMP_DIR, 3, new HashMap<>())) {
- barReader.getDocIds("retain hold");
+ assertEquals(barReader.getDocIds("retain", null).toArray(), new
int[]{2});
}
+ assertTrue(
+ new File(TEMP_DIR, "foo" +
V1Constants.Indexes.LUCENE_TEXT_INDEX_DOCID_MAPPING_FILE_EXTENSION).exists());
// Both files for TextIndex should be removed.
sfd.removeIndex("foo", StandardIndexes.text());
assertFalse(new File(TEMP_DIR, "foo" +
V1Constants.Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION).exists());
@@ -296,6 +309,7 @@ public class SingleFileIndexDirectoryTest implements
PinotBuffersAfterMethodChec
// Check if the text index still work.
try (LuceneTextIndexReader barReader = new LuceneTextIndexReader("bar",
TEMP_DIR, 3, new HashMap<>())) {
+ assertEquals(barReader.getDocIds("retain", null).toArray(), new
int[]{2});
MutableRoaringBitmap ids = barReader.getDocIds("retain hold");
assertTrue(ids.contains(0));
assertTrue(ids.contains(2));
diff --git
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/TextIndexConfig.java
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/TextIndexConfig.java
index 347aaaa146a..47e6b806009 100644
---
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/TextIndexConfig.java
+++
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/TextIndexConfig.java
@@ -78,11 +78,11 @@ public class TextIndexConfig extends IndexConfig {
private final boolean _storeInSegmentFile;
public enum DocIdTranslatorMode {
- // build and keep mapping
+ /// Keep load-time and reused-mutable mappings. Fresh immutable indexes
still drop verified identity mappings.
Default,
- // build mapping and check if it can be dropped
+ /// Drop verified identity mappings, including when reusing mutable
indexes.
TryOptimize,
- // mapping is not necessary, use lucene ids
+ /// Use Lucene document IDs without constructing or verifying a mapping.
Skip;
static DocIdTranslatorMode of(String mode) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]