Skip to content

Commit bba7aee

Browse files
authored
Enable Faiss-based vector format to index larger number of vectors in a single segment (#14847)
* Enable Faiss-based vector format to index larger number of vectors in a single segment - Moves away from a ByteBuffer (with a 2 GB limit) to direct copying of vectors to native memory - Also simplify some other off-heap memory IO instances * Add test * Mark test as "monster" - Also modify the test to make backporting easier * Allocate filtered doc bits without ByteBuffer * Add CHANGES.txt entry --------- Co-authored-by: Kaival Parikh <kaivalp2000@gmail.com>
1 parent 3f71b54 commit bba7aee

3 files changed

Lines changed: 55 additions & 37 deletions

File tree

lucene/CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ Bug Fixes
6464
returns a string which can be parsed back into the original node.
6565
(Peter Barna, Adam Schwartz)
6666

67+
* GITHUB#14847: Allow Faiss vector format to index >2GB of vectors per-field per-segment by using MemorySegment APIs
68+
(instead of ByteBuffer) to copy bytes to native memory. (Kaival Parikh)
69+
6770
Changes in Runtime Behavior
6871
---------------------
6972
* GITHUB#14187: The query cache is now disabled by default. (Adrien Grand)

lucene/sandbox/src/java/org/apache/lucene/sandbox/codecs/faiss/LibFaissC.java

Lines changed: 22 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.lucene.sandbox.codecs.faiss;
1818

1919
import static java.lang.foreign.ValueLayout.ADDRESS;
20+
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
2021
import static java.lang.foreign.ValueLayout.JAVA_FLOAT;
2122
import static java.lang.foreign.ValueLayout.JAVA_INT;
2223
import static java.lang.foreign.ValueLayout.JAVA_LONG;
@@ -32,8 +33,6 @@
3233
import java.lang.invoke.MethodHandles;
3334
import java.lang.invoke.MethodType;
3435
import java.nio.ByteOrder;
35-
import java.nio.FloatBuffer;
36-
import java.nio.LongBuffer;
3736
import java.util.Arrays;
3837
import java.util.Locale;
3938
import org.apache.lucene.index.FloatVectorValues;
@@ -221,16 +220,22 @@ public static MemorySegment createIndex(
221220

222221
// Allocate docs in native memory
223222
MemorySegment docs = temp.allocate(JAVA_FLOAT, (long) size * dimension);
224-
FloatBuffer docsBuffer = docs.asByteBuffer().order(ByteOrder.nativeOrder()).asFloatBuffer();
223+
long docsOffset = 0;
224+
long perDocByteSize = dimension * JAVA_FLOAT.byteSize();
225225

226226
// Allocate ids in native memory
227227
MemorySegment ids = temp.allocate(JAVA_LONG, size);
228-
LongBuffer idsBuffer = ids.asByteBuffer().order(ByteOrder.nativeOrder()).asLongBuffer();
228+
int idsIndex = 0;
229229

230230
KnnVectorValues.DocIndexIterator iterator = floatVectorValues.iterator();
231231
for (int i = iterator.nextDoc(); i != NO_MORE_DOCS; i = iterator.nextDoc()) {
232-
idsBuffer.put(oldToNewDocId.apply(i));
233-
docsBuffer.put(floatVectorValues.vectorValue(iterator.index()));
232+
int id = oldToNewDocId.apply(i);
233+
ids.setAtIndex(JAVA_LONG, idsIndex, id);
234+
idsIndex++;
235+
236+
float[] vector = floatVectorValues.vectorValue(iterator.index());
237+
MemorySegment.copy(vector, 0, docs, JAVA_FLOAT, docsOffset, vector.length);
238+
docsOffset += perDocByteSize;
234239
}
235240

236241
// Train index
@@ -254,18 +259,12 @@ private static long writeBytes(
254259
inputPointer = inputPointer.reinterpret(size);
255260

256261
if (size <= BUFFER_SIZE) { // simple case, avoid buffering
257-
byte[] bytes = new byte[(int) size];
258-
inputPointer.asSlice(0, size).asByteBuffer().order(ByteOrder.nativeOrder()).get(bytes);
259-
output.writeBytes(bytes, bytes.length);
262+
output.writeBytes(inputPointer.toArray(JAVA_BYTE), (int) size);
260263
} else { // copy buffered number of bytes repeatedly
261264
byte[] bytes = new byte[BUFFER_SIZE];
262265
for (long offset = 0; offset < size; offset += BUFFER_SIZE) {
263266
int length = (int) Math.min(size - offset, BUFFER_SIZE);
264-
inputPointer
265-
.asSlice(offset, length)
266-
.asByteBuffer()
267-
.order(ByteOrder.nativeOrder())
268-
.get(bytes, 0, length);
267+
MemorySegment.copy(inputPointer, JAVA_BYTE, offset, bytes, 0, length);
269268
output.writeBytes(bytes, length);
270269
}
271270
}
@@ -282,21 +281,13 @@ private static long readBytes(
282281
if (size <= BUFFER_SIZE) { // simple case, avoid buffering
283282
byte[] bytes = new byte[(int) size];
284283
input.readBytes(bytes, 0, bytes.length);
285-
outputPointer
286-
.asSlice(0, bytes.length)
287-
.asByteBuffer()
288-
.order(ByteOrder.nativeOrder())
289-
.put(bytes);
284+
MemorySegment.copy(bytes, 0, outputPointer, JAVA_BYTE, 0, bytes.length);
290285
} else { // copy buffered number of bytes repeatedly
291286
byte[] bytes = new byte[BUFFER_SIZE];
292287
for (long offset = 0; offset < size; offset += BUFFER_SIZE) {
293288
int length = (int) Math.min(size - offset, BUFFER_SIZE);
294289
input.readBytes(bytes, 0, length);
295-
outputPointer
296-
.asSlice(offset, length)
297-
.asByteBuffer()
298-
.order(ByteOrder.nativeOrder())
299-
.put(bytes, 0, length);
290+
MemorySegment.copy(bytes, 0, outputPointer, JAVA_BYTE, offset, length);
300291
}
301292
}
302293
return numItems;
@@ -411,8 +402,7 @@ public static void indexSearch(
411402
};
412403

413404
// Allocate queries in native memory
414-
MemorySegment queries = temp.allocate(JAVA_FLOAT, query.length);
415-
queries.asByteBuffer().order(ByteOrder.nativeOrder()).asFloatBuffer().put(query);
405+
MemorySegment queries = temp.allocateFrom(JAVA_FLOAT, query);
416406

417407
// Faiss knn search
418408
int k = knnCollector.k();
@@ -427,10 +417,9 @@ public static void indexSearch(
427417
MemorySegment pointer = temp.allocate(ADDRESS);
428418

429419
long[] bits = fixedBitSet.getBits();
430-
MemorySegment nativeBits = temp.allocate(JAVA_LONG, bits.length);
431-
432-
// Use LITTLE_ENDIAN to convert long[] -> uint8_t*
433-
nativeBits.asByteBuffer().order(ByteOrder.LITTLE_ENDIAN).asLongBuffer().put(bits);
420+
MemorySegment nativeBits =
421+
// Use LITTLE_ENDIAN to convert long[] -> uint8_t*
422+
temp.allocateFrom(JAVA_LONG.withOrder(ByteOrder.LITTLE_ENDIAN), bits);
434423

435424
callAndHandleError(ID_SELECTOR_BITMAP_NEW, pointer, fixedBitSet.length(), nativeBits);
436425
MemorySegment idSelectorBitmapPointer =
@@ -458,13 +447,9 @@ public static void indexSearch(
458447
idsPointer);
459448
}
460449

461-
// Retrieve scores
462-
float[] distances = new float[k];
463-
distancesPointer.asByteBuffer().order(ByteOrder.nativeOrder()).asFloatBuffer().get(distances);
464-
465-
// Retrieve ids
466-
long[] ids = new long[k];
467-
idsPointer.asByteBuffer().order(ByteOrder.nativeOrder()).asLongBuffer().get(ids);
450+
// Retrieve scores and ids
451+
float[] distances = distancesPointer.toArray(JAVA_FLOAT);
452+
long[] ids = idsPointer.toArray(JAVA_LONG);
468453

469454
// Record hits
470455
for (int i = 0; i < k; i++) {

lucene/sandbox/src/test/org/apache/lucene/sandbox/codecs/faiss/TestFaissKnnVectorsFormat.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@
2121
import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN;
2222

2323
import java.io.IOException;
24+
import java.util.Collections;
25+
import java.util.List;
2426
import org.apache.lucene.codecs.Codec;
27+
import org.apache.lucene.codecs.KnnVectorsFormat;
28+
import org.apache.lucene.document.KnnFloatVectorField;
29+
import org.apache.lucene.index.IndexWriter;
30+
import org.apache.lucene.index.IndexWriterConfig;
2531
import org.apache.lucene.index.VectorEncoding;
2632
import org.apache.lucene.index.VectorSimilarityFunction;
33+
import org.apache.lucene.store.Directory;
2734
import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase;
2835
import org.apache.lucene.tests.util.TestUtil;
2936
import org.junit.BeforeClass;
@@ -108,4 +115,27 @@ public void testEmptyByteVectorData() {}
108115
@Override
109116
@Ignore // does not support byte vectors
110117
public void testMergingWithDifferentByteKnnFields() {}
118+
119+
@Monster("Uses large amount of heap and RAM")
120+
public void testLargeVectorData() throws IOException {
121+
KnnVectorsFormat format =
122+
new FaissKnnVectorsFormat(
123+
"IDMap,Flat", // no need for special indexing like HNSW
124+
"");
125+
IndexWriterConfig config =
126+
newIndexWriterConfig().setCodec(TestUtil.alwaysKnnVectorsFormat(format));
127+
128+
float[] largeVector =
129+
new float[format.getMaxDimensions("vector")]; // largest vector accepted by the format
130+
int numDocs =
131+
Math.ceilDivExact(
132+
Integer.MAX_VALUE, Float.BYTES * largeVector.length); // find minimum number of docs
133+
134+
// Check that we can index vectors larger than Integer.MAX_VALUE number of bytes
135+
try (Directory directory = newDirectory();
136+
IndexWriter writer = new IndexWriter(directory, config)) {
137+
writer.addDocuments(
138+
Collections.nCopies(numDocs, List.of(new KnnFloatVectorField("vector", largeVector))));
139+
}
140+
}
111141
}

0 commit comments

Comments
 (0)