3939import org .apache .lucene .search .DocIdSetIterator ;
4040import org .apache .lucene .search .VectorScorer ;
4141import org .apache .lucene .store .ChecksumIndexInput ;
42+ import org .apache .lucene .store .DataAccessHint ;
43+ import org .apache .lucene .store .FileDataHint ;
44+ import org .apache .lucene .store .FileTypeHint ;
45+ import org .apache .lucene .store .IOContext ;
4246import org .apache .lucene .store .IndexInput ;
4347import org .apache .lucene .util .IOUtils ;
4448import org .apache .lucene .util .hnsw .RandomVectorScorer ;
@@ -59,6 +63,13 @@ final class DedupFlatVectorsReader extends FlatVectorsReader {
5963 private final FieldInfos fieldInfos ;
6064 private final IndexInput data ;
6165
66+ /**
67+ * IO context used to open {@link #data}. Captured so that {@link #getMergeInstance()} can switch
68+ * the underlying access hint to {@link DataAccessHint#SEQUENTIAL} for the duration of a merge,
69+ * and {@link #finishMerge()} can revert it back to the original context for search.
70+ */
71+ private final IOContext dataContext ;
72+
6273 DedupFlatVectorsReader (SegmentReadState state , FlatVectorsScorer scorer ) throws IOException {
6374 super (scorer );
6475 this .fieldInfos = state .fieldInfos ;
@@ -71,7 +82,8 @@ final class DedupFlatVectorsReader extends FlatVectorsReader {
7182 String dataFileName =
7283 IndexFileNames .segmentFileName (
7384 state .segmentInfo .name , state .segmentSuffix , DedupFlatVectorsFormat .DATA_EXTENSION );
74- IndexInput dataIn = state .directory .openInput (dataFileName , state .context );
85+ this .dataContext = state .context .withHints (FileTypeHint .DATA , FileDataHint .KNN_VECTORS );
86+ IndexInput dataIn = state .directory .openInput (dataFileName , dataContext );
7587 try {
7688 int versionData =
7789 CodecUtil .checkIndexHeader (
@@ -223,16 +235,83 @@ public RandomVectorScorer getRandomVectorScorer(String field, byte[] target) thr
223235 return vectorScorer .getRandomVectorScorer (entry .simFunc , getByteVectorValues (field ), target );
224236 }
225237
238+ // ---- Package-private accessors for merge-path optimization ----
239+ //
240+ // These let a dedup-aware writer reuse this reader's existing dictionary + ordToDict mapping
241+ // instead of hashing each per-doc vector individually at merge time. See
242+ // DedupFlatVectorsWriter#buildFloatSubs / #buildByteSubs.
243+
244+ /**
245+ * Return the per-doc {@code ordToDict} mapping for the given field, or {@code null} if the field
246+ * has no duplicates (identity mapping — no value in trying to skip hashing since there are no
247+ * duplicates to collapse).
248+ */
249+ int [] getOrdToDict (String field ) {
250+ return getFieldEntry (field ).ordToDict ;
251+ }
252+
253+ /**
254+ * Return a random-access view over the {@code dictSize} unique float vectors in the field's
255+ * dictionary, indexed by source dict ord. The merge writer walks this once, hashes each vector
256+ * into its own target dict, and builds a {@code sourceDictOrd → targetDictOrd} mapping — which it
257+ * then uses to resolve every per-doc ord with a single array lookup (no per-doc hashing or
258+ * equality check).
259+ *
260+ * <p>Returns {@code null} if the field's encoding is not float32.
261+ */
262+ FloatVectorValues getDictFloatVectorValues (String field ) throws IOException {
263+ FieldEntry entry = getFieldEntry (field );
264+ if (entry .encoding != VectorEncoding .FLOAT32 ) {
265+ return null ;
266+ }
267+ IndexInput dictSlice = data .slice ("dedup-dict" , entry .dictDataOffset , entry .dictDataLength ());
268+ return new OffHeapFloatVectorValues .DenseOffHeapVectorValues (
269+ entry .dictDimension ,
270+ entry .dictSize ,
271+ dictSlice ,
272+ entry .vectorByteSize ,
273+ vectorScorer ,
274+ entry .simFunc );
275+ }
276+
277+ /** Byte-encoded mirror of {@link #getDictFloatVectorValues(String)}. */
278+ ByteVectorValues getDictByteVectorValues (String field ) throws IOException {
279+ FieldEntry entry = getFieldEntry (field );
280+ if (entry .encoding != VectorEncoding .BYTE ) {
281+ return null ;
282+ }
283+ IndexInput dictSlice = data .slice ("dedup-dict" , entry .dictDataOffset , entry .dictDataLength ());
284+ return new OffHeapByteVectorValues .DenseOffHeapVectorValues (
285+ entry .dictDimension ,
286+ entry .dictSize ,
287+ dictSlice ,
288+ entry .vectorByteSize ,
289+ vectorScorer ,
290+ entry .simFunc );
291+ }
292+
226293 @ Override
227294 public void checkIntegrity () throws IOException {
228295 CodecUtil .checksumEntireFile (data );
229296 }
230297
231298 @ Override
232- public FlatVectorsReader getMergeInstance () {
299+ public FlatVectorsReader getMergeInstance () throws IOException {
300+ // During merge, the dedup writer walks each source segment's vectors mostly in forward order
301+ // (via DocIDMerger). Switch the IO hint to SEQUENTIAL so the directory can enable readahead.
302+ // Occasional random-access reads for hash-collision verification target recently-read vectors,
303+ // which typically remain resident in the page cache, so they don't materially suffer from the
304+ // SEQUENTIAL hint.
305+ data .updateIOContext (dataContext .withHints (DataAccessHint .SEQUENTIAL ));
233306 return this ;
234307 }
235308
309+ @ Override
310+ public void finishMerge () throws IOException {
311+ // Revert the access hint to the original context used for search.
312+ data .updateIOContext (dataContext );
313+ }
314+
236315 @ Override
237316 public void close () throws IOException {
238317 IOUtils .close (data );
0 commit comments