This is an automated email from the ASF dual-hosted git repository.

Jackie-Jiang 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 14235e1bc27 Skip iterative File.exists() check while answering which 
column has text index and vector index (#19212)
14235e1bc27 is described below

commit 14235e1bc272c399270ddf80566c6898acba249c
Author: Jhow <[email protected]>
AuthorDate: Wed Aug 12 02:31:54 2026 +0800

    Skip iterative File.exists() check while answering which column has text 
index and vector index (#19212)
---
 .../loader/invertedindex/TextIndexHandler.java     | 18 +++-----
 .../segment/store/SingleFileIndexDirectory.java    | 12 +----
 .../local/segment/store/TextIndexUtils.java        | 44 ++++++++++++++++++
 .../local/segment/store/VectorIndexUtils.java      | 35 ++++++++++++++
 .../store/SingleFileIndexDirectoryTest.java        | 54 ++++++++++++++++++++++
 5 files changed, 142 insertions(+), 21 deletions(-)

diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/invertedindex/TextIndexHandler.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/invertedindex/TextIndexHandler.java
index 0b49ab82937..008de7b9513 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/invertedindex/TextIndexHandler.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/invertedindex/TextIndexHandler.java
@@ -20,6 +20,7 @@ package 
org.apache.pinot.segment.local.segment.index.loader.invertedindex;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -382,18 +383,13 @@ public class TextIndexHandler extends BaseIndexHandler {
     File indexDir = _segmentDirectory.getSegmentMetadata().getIndexDir();
     File segmentDirectory =
         SegmentDirectoryPaths.segmentDirectoryFor(indexDir, 
_segmentDirectory.getSegmentMetadata().getVersion());
-    Set<String> columns = new HashSet<>();
-    for (String column : 
_segmentDirectory.getSegmentMetadata().getAllColumns()) {
-      if (hasLegacyNativeTextIndex(indexDir, segmentDirectory, column)) {
-        columns.add(column);
-      }
+    Collection<String> allColumns = 
_segmentDirectory.getSegmentMetadata().getAllColumns();
+    Set<String> columns = new 
HashSet<>(TextIndexUtils.getColumnsWithLegacyNativeTextIndex(indexDir, 
allColumns));
+    // Preserves the original short-circuit: the version subdirectory is only 
consulted when it is a
+    // different directory from the segment root.
+    if (!segmentDirectory.equals(indexDir)) {
+      
columns.addAll(TextIndexUtils.getColumnsWithLegacyNativeTextIndex(segmentDirectory,
 allColumns));
     }
     return columns;
   }
-
-  private boolean hasLegacyNativeTextIndex(File indexDir, File 
segmentDirectory, String column) {
-    String legacyNativeTextIndexFile = column + 
V1Constants.Indexes.DEPRECATED_NATIVE_TEXT_INDEX_FILE_EXTENSION;
-    return new File(indexDir, legacyNativeTextIndexFile).exists()
-        || (!segmentDirectory.equals(indexDir) && new File(segmentDirectory, 
legacyNativeTextIndexFile).exists());
-  }
 }
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/SingleFileIndexDirectory.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/SingleFileIndexDirectory.java
index 3afcdb12b3e..2b4f1e6b3cc 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/SingleFileIndexDirectory.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/SingleFileIndexDirectory.java
@@ -523,21 +523,13 @@ class SingleFileIndexDirectory extends 
ColumnIndexDirectory {
     Set<String> columns = new HashSet<>();
     // TEXT_INDEX is not tracked via _columnEntries, so handled separately.
     if (type == StandardIndexes.text()) {
-      for (String column : _segmentMetadata.getAllColumns()) {
-        if (TextIndexUtils.hasTextIndex(_segmentDirectory, column)) {
-          columns.add(column);
-        }
-      }
+      columns.addAll(TextIndexUtils.getColumnsWithTextIndex(_segmentDirectory, 
_segmentMetadata.getAllColumns()));
     }
     if (type == StandardIndexes.vector()) {
       // Vector may live as a combined file (legacy / 
storeInSegmentFile=false) or as a typed
       // entry in columns.psf (storeInSegmentFile=true). Collect both. Removed 
the early-return
       // that previously hid consolidated entries from this view.
-      for (String column : _segmentMetadata.getAllColumns()) {
-        if (VectorIndexUtils.hasVectorIndex(_segmentDirectory, column)) {
-          columns.add(column);
-        }
-      }
+      
columns.addAll(VectorIndexUtils.getColumnsWithVectorIndex(_segmentDirectory, 
_segmentMetadata.getAllColumns()));
     }
     for (IndexKey indexKey : _columnEntries.keySet()) {
       if (indexKey._type == type) {
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 c5b56825733..87ce1c3e836 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
@@ -22,6 +22,7 @@ import java.io.File;
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -113,6 +114,49 @@ public class TextIndexUtils {
     //@formatter:on
   }
 
+  /// Bulk form of [#hasTextIndex]: which of `columns` have a Lucene text 
index in `segDir`.
+  ///
+  /// Prefer this over calling [#hasTextIndex] in a loop over a segment's 
columns. It lists the
+  /// directory once rather than performing `columns × extensions` existence 
probes. Finding
+  /// `<column><extension>` in the listing is equivalent to that path 
existing, so the answer is the
+  /// same.
+  public static Set<String> getColumnsWithTextIndex(File segDir, 
Collection<String> columns) {
+    Set<String> entries = listEntryNames(segDir);
+    Set<String> columnsWithIndex = new HashSet<>();
+    for (String column : columns) {
+      if (entries.contains(column + Indexes.LUCENE_TEXT_INDEX_FILE_EXTENSION)
+          || entries.contains(column + 
Indexes.LUCENE_V9_TEXT_INDEX_FILE_EXTENSION)
+          || entries.contains(column + 
Indexes.LUCENE_V99_TEXT_INDEX_FILE_EXTENSION)
+          || entries.contains(column + 
Indexes.LUCENE_V912_TEXT_INDEX_FILE_EXTENSION)) {
+        columnsWithIndex.add(column);
+      }
+    }
+    return columnsWithIndex;
+  }
+
+  /// Which of `columns` still have a deprecated native text index file in 
`segDir`.
+  ///
+  /// Listing-based for the same reason as [#getColumnsWithTextIndex]: the 
caller asks this for every
+  /// column of a segment on every reload check, including the overwhelmingly 
common case where no
+  /// column has ever had a native text index.
+  public static Set<String> getColumnsWithLegacyNativeTextIndex(File segDir, 
Collection<String> columns) {
+    Set<String> entries = listEntryNames(segDir);
+    Set<String> columnsWithIndex = new HashSet<>();
+    for (String column : columns) {
+      if (entries.contains(column + 
Indexes.DEPRECATED_NATIVE_TEXT_INDEX_FILE_EXTENSION)) {
+        columnsWithIndex.add(column);
+      }
+    }
+    return columnsWithIndex;
+  }
+
+  /// Names of the entries directly inside `dir`, files and directories alike 
(a Lucene text index is
+  /// a directory), or empty when it cannot be listed.
+  private static Set<String> listEntryNames(File dir) {
+    String[] names = dir.list();
+    return names == null ? Set.of() : new HashSet<>(Arrays.asList(names));
+  }
+
   public static List<String> extractStopWordsInclude(String colName,
       Map<String, Map<String, String>> columnProperties) {
     return extractStopWordsInclude(columnProperties.getOrDefault(colName, 
null));
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/VectorIndexUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/VectorIndexUtils.java
index e50d8dc15af..ecd54e4a995 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/VectorIndexUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/store/VectorIndexUtils.java
@@ -22,6 +22,10 @@ import com.google.common.annotations.VisibleForTesting;
 import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
 import javax.annotation.Nullable;
 import org.apache.commons.io.FileUtils;
 import org.apache.lucene.codecs.lucene912.Lucene912Codec;
@@ -100,6 +104,37 @@ public class VectorIndexUtils {
         || new File(segDir, column + 
Indexes.VECTOR_IVF_PQ_INDEX_FILE_EXTENSION).exists();
   }
 
+  /// Bulk form of [#hasVectorIndex]: which of `columns` have a vector index 
sidecar in `segDir`.
+  ///
+  /// Prefer this over calling [#hasVectorIndex] in a loop over a segment's 
columns. It lists the
+  /// directory once rather than performing `columns × extensions` existence 
probes. Finding
+  /// `<column><extension>` in the listing is equivalent to that path 
existing, so the answer is the
+  /// same.
+  public static Set<String> getColumnsWithVectorIndex(File segDir, 
Collection<String> columns) {
+    Set<String> entries = listEntryNames(segDir);
+    Set<String> columnsWithIndex = new HashSet<>();
+    for (String column : columns) {
+      if (entries.contains(column + Indexes.VECTOR_HNSW_INDEX_FILE_EXTENSION)
+          || entries.contains(column + 
Indexes.VECTOR_V99_HNSW_INDEX_FILE_EXTENSION)
+          || entries.contains(column + 
Indexes.VECTOR_V912_HNSW_INDEX_FILE_EXTENSION)
+          || entries.contains(column + Indexes.VECTOR_INDEX_FILE_EXTENSION)
+          || entries.contains(column + Indexes.VECTOR_V99_INDEX_FILE_EXTENSION)
+          || entries.contains(column + 
Indexes.VECTOR_V912_INDEX_FILE_EXTENSION)
+          || entries.contains(column + 
Indexes.VECTOR_IVF_FLAT_INDEX_FILE_EXTENSION)
+          || entries.contains(column + 
Indexes.VECTOR_IVF_PQ_INDEX_FILE_EXTENSION)) {
+        columnsWithIndex.add(column);
+      }
+    }
+    return columnsWithIndex;
+  }
+
+  /// Names of the entries directly inside `dir`, files and directories alike 
(an HNSW vector index is
+  /// a Lucene directory), or empty when it cannot be listed.
+  private static Set<String> listEntryNames(File dir) {
+    String[] names = dir.list();
+    return names == null ? Set.of() : new HashSet<>(Arrays.asList(names));
+  }
+
   /// Returns `true` when the V1/V2 segment directory holds an IVF vector 
index in the
   /// combined-form extension (`.vector.ivfflat.combined.index` or
   /// `.vector.ivfpq.combined.index`). The combined form is written by an IVF 
creator run
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 92063a42c7c..3825363d5f9 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
@@ -408,4 +408,58 @@ public class SingleFileIndexDirectoryTest implements 
PinotBuffersAfterMethodChec
           new HashSet<>(List.of("bar")));
     }
   }
+
+  /// Vector indexes may live either as a sibling file next to `columns.psf` 
or as a typed entry
+  /// inside it, so `getColumnsWithIndex(vector())` has to union a filesystem 
view with
+  /// `_columnEntries`. Discovery is purely name-based 
(`<column><extension>`), which is what this
+  /// asserts — including the two ways a name-based scan can go wrong: 
crediting a sidecar to a column
+  /// that is not in the segment, and mis-parsing a file whose name merely 
*contains* an extension.
+  @Test
+  public void testGetColumnsWithVectorIndex()
+      throws Exception {
+    // Sidecar forms, one per extension family, to cover more than a single 
spelling.
+    createEmptyFile("hnswCol" + 
V1Constants.Indexes.VECTOR_HNSW_INDEX_FILE_EXTENSION);
+    createEmptyFile("ivfCol" + 
V1Constants.Indexes.VECTOR_IVF_FLAT_INDEX_FILE_EXTENSION);
+    // Lucene-backed vector indexes are directories, not files — both must be 
discovered.
+    FileUtils.forceMkdir(new File(TEMP_DIR, "dirCol" + 
V1Constants.Indexes.VECTOR_V912_INDEX_FILE_EXTENSION));
+    // A sidecar for a column the segment does not declare must not be 
reported.
+    createEmptyFile("strayCol" + 
V1Constants.Indexes.VECTOR_HNSW_INDEX_FILE_EXTENSION);
+    // A name that merely contains an extension is not "<column><extension>" 
for any real column, so
+    // it must not be credited to anything. This is the case a 
suffix-stripping scan gets wrong.
+    createEmptyFile("notAColumn" + 
V1Constants.Indexes.VECTOR_INDEX_FILE_EXTENSION + ".bak");
+
+    try (SingleFileIndexDirectory sfd = new SingleFileIndexDirectory(TEMP_DIR, 
_segmentMetadata, ReadMode.mmap)) {
+      // A consolidated vector index — tracked in _columnEntries rather than 
on disk.
+      PinotDataBuffer buf = sfd.newBuffer("psfCol", StandardIndexes.vector(), 
1024);
+      buf.putInt(0, 777);
+    }
+
+    when(_segmentMetadata.getAllColumns()).thenReturn(
+        new TreeSet<>(Arrays.asList("hnswCol", "ivfCol", "dirCol", "psfCol", 
"plainCol")));
+    try (SingleFileIndexDirectory sfd = new SingleFileIndexDirectory(TEMP_DIR, 
_segmentMetadata, ReadMode.mmap)) {
+      assertEquals(sfd.getColumnsWithIndex(StandardIndexes.vector()),
+          new HashSet<>(Arrays.asList("hnswCol", "ivfCol", "dirCol", 
"psfCol")));
+    }
+  }
+
+  /// A segment with no vector index anywhere reports none — the common case, 
and the one that used to
+  /// cost `columns × extensions` existence probes to answer.
+  @Test
+  public void testGetColumnsWithVectorIndexWhenAbsent()
+      throws Exception {
+    try (SingleFileIndexDirectory sfd = new SingleFileIndexDirectory(TEMP_DIR, 
_segmentMetadata, ReadMode.mmap)) {
+      PinotDataBuffer buf = sfd.newBuffer("col1", StandardIndexes.forward(), 
1024);
+      buf.putInt(0, 111);
+    }
+    when(_segmentMetadata.getAllColumns()).thenReturn(new 
TreeSet<>(Arrays.asList("col1", "col2")));
+    try (SingleFileIndexDirectory sfd = new SingleFileIndexDirectory(TEMP_DIR, 
_segmentMetadata, ReadMode.mmap)) {
+      assertEquals(sfd.getColumnsWithIndex(StandardIndexes.vector()), 
Set.of());
+      assertEquals(sfd.getColumnsWithIndex(StandardIndexes.text()), Set.of());
+    }
+  }
+
+  private static void createEmptyFile(String name)
+      throws IOException {
+    FileUtils.touch(new File(TEMP_DIR, name));
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to