Skip to content

Commit 078e915

Browse files
committed
iter
1 parent 53854e4 commit 078e915

5 files changed

Lines changed: 978 additions & 496 deletions

File tree

lucene/core/src/java/org/apache/lucene/codecs/dedup/DedupFlatVectorsFormat.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,18 @@
4747
* <li>-1 sentinel marking end of fields
4848
* </ul>
4949
*
50-
* <p>During merge, vectors are streamed one at a time. Only a hash map of {@code long hash → int
51-
* dictOrd} is held in memory (~12 bytes per unique vector). Vector data is never buffered.
50+
* <p>The writer performs no temporary-file IO. Dictionary bytes stream directly into the main
51+
* {@code .dvd} output. Hash-collision verification uses already-resident sources: on-heap typed
52+
* arrays during flush, and already-open {@link org.apache.lucene.codecs.KnnVectorsReader}s from
53+
* {@link org.apache.lucene.index.MergeState} during merge.
54+
*
55+
* <p>When a source segment being merged was itself written with this format, the merge path skips
56+
* per-doc hashing through a lazy {@code sourceDictOrd → targetDictOrd} cache and a same-sub int-ord
57+
* shortcut on hash collision. This also drops source dict entries whose referencing docs have all
58+
* been deleted — their dict ords are never touched, so dead vectors do not accumulate across merge
59+
* cycles.
60+
*
61+
* <p>See {@code lucene/dev-docs/dedup-vectors-format-design.md} for the full design.
5262
*
5363
* @lucene.experimental
5464
*/

lucene/core/src/java/org/apache/lucene/codecs/dedup/DedupFlatVectorsReader.java

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
import org.apache.lucene.search.DocIdSetIterator;
4040
import org.apache.lucene.search.VectorScorer;
4141
import 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;
4246
import org.apache.lucene.store.IndexInput;
4347
import org.apache.lucene.util.IOUtils;
4448
import 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

Comments
 (0)