Skip to content

Commit 755ee43

Browse files
committed
Add config to disable leading segment merge
Also tweak some logs Signed-off-by: Ashwin Krishna Kumar <nebulousmagneticwind@outlook.com>
1 parent 9373b41 commit 755ee43

10 files changed

Lines changed: 120 additions & 15 deletions

File tree

src/main/java/org/opensearch/knn/common/KNNConstants.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ public class KNNConstants {
100100
// default
101101
public static final Boolean DEFAULT_HIERARCHY_ENABLED = false;
102102

103+
// Parameters that only affect merge
104+
public static final String METHOD_PARAMETER_LEADING_SEGMENT_MERGE_DISABLED = "advanced.leading_segment_merge_disabled";
105+
public static final boolean DEFAULT_LEADING_SEGMENT_MERGE_DISABLED = false;
106+
103107
// API Constants
104108
public static final String CLEAR_CACHE = "clear_cache";
105109

src/main/java/org/opensearch/knn/index/codec/KNN9120Codec/KNN9120PerFieldKnnVectorsFormat.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ public KNN9120PerFieldKnnVectorsFormat(final Optional<MapperService> mapperServi
7070
knnVectorsFormatParams.getNeighborOverflow(),
7171
knnVectorsFormatParams.getNumberOfSubspacesPerVectorSupplier(),
7272
knnVectorsFormatParams.getMinBatchSizeForQuantization(),
73-
knnVectorsFormatParams.isHierarchyEnabled()
73+
knnVectorsFormatParams.isHierarchyEnabled(),
74+
knnVectorsFormatParams.isLeadingSegmentMergeDisabled()
7475
);
7576
default:
7677
throw new IllegalArgumentException("Unsupported java engine: " + knnEngine);

src/main/java/org/opensearch/knn/index/codec/jvector/GraphNodeIdToDocMap.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ public GraphNodeIdToDocMap(int[] graphNodeIdsToDocIds) {
7272
final int maxDocs = maxDocId + 1;
7373
// We are going to assume that the number of ordinals is roughly the same as the number of documents in the segment, therefore,
7474
// the mapping will not be sparse.
75+
// Note that the merge process may create ephemeral mappings that are sparse.
7576
if (maxDocs < 0.8 * graphNodeIdsToDocIds.length) {
7677
log.info(
77-
"Max docs {} is less than 80% the number of ordinals {}, this implies a lot of deleted documents. Or that some documents are missing vectors. Wasting a lot of memory",
78+
"Max docs {} is less than 80% the number of ordinals {}. This is normal if many docs were recently deleted or overwritten. Otherwise if many docs are missing vectors this is a waste of memory.",
7879
maxDocs,
7980
graphNodeIdsToDocIds.length
8081
);

src/main/java/org/opensearch/knn/index/codec/jvector/JVectorDiskANNMethod.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ private static MethodComponent initMethodComponent() {
8585
(v, context) -> v != null && v > 0 && v <= context.getDimension()
8686
)
8787
)
88+
.addParameter(
89+
METHOD_PARAMETER_LEADING_SEGMENT_MERGE_DISABLED,
90+
new Parameter.BooleanParameter(
91+
METHOD_PARAMETER_LEADING_SEGMENT_MERGE_DISABLED,
92+
DEFAULT_LEADING_SEGMENT_MERGE_DISABLED,
93+
(v, context) -> true
94+
)
95+
)
8896
.build();
8997
}
9098

src/main/java/org/opensearch/knn/index/codec/jvector/JVectorFormat.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class JVectorFormat extends KnnVectorsFormat {
4545
private final float alpha;
4646
private final float neighborOverflow;
4747
private final boolean hierarchyEnabled;
48+
private final boolean leadingSegmentMergeDisabled;
4849

4950
public JVectorFormat() {
5051
this(
@@ -55,11 +56,16 @@ public JVectorFormat() {
5556
KNNConstants.DEFAULT_ALPHA_VALUE.floatValue(),
5657
JVectorFormat::getDefaultNumberOfSubspacesPerVector,
5758
KNNConstants.DEFAULT_MINIMUM_BATCH_SIZE_FOR_QUANTIZATION,
58-
KNNConstants.DEFAULT_HIERARCHY_ENABLED
59+
KNNConstants.DEFAULT_HIERARCHY_ENABLED,
60+
KNNConstants.DEFAULT_LEADING_SEGMENT_MERGE_DISABLED
5961
);
6062
}
6163

6264
public JVectorFormat(int minBatchSizeForQuantization) {
65+
this(minBatchSizeForQuantization, KNNConstants.DEFAULT_LEADING_SEGMENT_MERGE_DISABLED);
66+
}
67+
68+
public JVectorFormat(int minBatchSizeForQuantization, boolean leadingSegmentMergeDisabled) {
6369
this(
6470
NAME,
6571
DEFAULT_MAX_CONN,
@@ -68,7 +74,8 @@ public JVectorFormat(int minBatchSizeForQuantization) {
6874
KNNConstants.DEFAULT_ALPHA_VALUE.floatValue(),
6975
JVectorFormat::getDefaultNumberOfSubspacesPerVector,
7076
minBatchSizeForQuantization,
71-
KNNConstants.DEFAULT_HIERARCHY_ENABLED
77+
KNNConstants.DEFAULT_HIERARCHY_ENABLED,
78+
leadingSegmentMergeDisabled
7279
);
7380
}
7481

@@ -79,7 +86,8 @@ public JVectorFormat(
7986
float alpha,
8087
Function<Integer, Integer> numberOfSubspacesPerVectorSupplier,
8188
int minBatchSizeForQuantization,
82-
boolean hierarchyEnabled
89+
boolean hierarchyEnabled,
90+
boolean leadingSegmentMergeDisabled
8391
) {
8492
this(
8593
NAME,
@@ -89,7 +97,8 @@ public JVectorFormat(
8997
alpha,
9098
numberOfSubspacesPerVectorSupplier,
9199
minBatchSizeForQuantization,
92-
hierarchyEnabled
100+
hierarchyEnabled,
101+
leadingSegmentMergeDisabled
93102
);
94103
}
95104

@@ -101,7 +110,8 @@ public JVectorFormat(
101110
float alpha,
102111
Function<Integer, Integer> numberOfSubspacesPerVectorSupplier,
103112
int minBatchSizeForQuantization,
104-
boolean hierarchyEnabled
113+
boolean hierarchyEnabled,
114+
boolean leadingSegmentMergeDisabled
105115
) {
106116
super(name);
107117
this.maxConn = maxConn;
@@ -111,6 +121,7 @@ public JVectorFormat(
111121
this.alpha = alpha;
112122
this.neighborOverflow = neighborOverflow;
113123
this.hierarchyEnabled = hierarchyEnabled;
124+
this.leadingSegmentMergeDisabled = leadingSegmentMergeDisabled;
114125
}
115126

116127
@Override
@@ -123,7 +134,8 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException
123134
alpha,
124135
numberOfSubspacesPerVectorSupplier,
125136
minBatchSizeForQuantization,
126-
hierarchyEnabled
137+
hierarchyEnabled,
138+
leadingSegmentMergeDisabled
127139
);
128140
}
129141

src/main/java/org/opensearch/knn/index/codec/jvector/JVectorWriter.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public class JVectorWriter extends KnnVectorsWriter {
9696
// as a function of the original dimension
9797
private final int minimumBatchSizeForQuantization; // Threshold for the vector count above which we will trigger PQ quantization
9898
private final boolean hierarchyEnabled;
99+
private final boolean leadingSegmentMergeDisabled;
99100

100101
private boolean finished = false;
101102

@@ -107,7 +108,8 @@ public JVectorWriter(
107108
float alpha,
108109
Function<Integer, Integer> numberOfSubspacesPerVectorSupplier,
109110
int minimumBatchSizeForQuantization,
110-
boolean hierarchyEnabled
111+
boolean hierarchyEnabled,
112+
boolean leadingSegmentMergeDisabled
111113
) throws IOException {
112114
this.segmentWriteState = segmentWriteState;
113115
this.maxConn = maxConn;
@@ -117,6 +119,8 @@ public JVectorWriter(
117119
this.numberOfSubspacesPerVectorSupplier = numberOfSubspacesPerVectorSupplier;
118120
this.minimumBatchSizeForQuantization = minimumBatchSizeForQuantization;
119121
this.hierarchyEnabled = hierarchyEnabled;
122+
this.leadingSegmentMergeDisabled = leadingSegmentMergeDisabled;
123+
120124
String metaFileName = IndexFileNames.segmentFileName(
121125
segmentWriteState.segmentInfo.name,
122126
segmentWriteState.segmentSuffix,
@@ -796,7 +800,7 @@ public RandomAccessMergedFloatVectorValues(FieldInfo fieldInfo, MergeState merge
796800

797801
for (int docId = it.nextDoc(); docId != DocIdSetIterator.NO_MORE_DOCS; docId = it.nextDoc()) {
798802
if (docMaps[readerIdx].get(docId) == -1) {
799-
log.warn(
803+
log.debug(
800804
"Document {} in reader {} is not mapped to a global ordinal from the merge docMaps. Will skip this document for now",
801805
docId,
802806
readerIdx
@@ -956,11 +960,21 @@ public void merge() throws IOException {
956960
/**
957961
* <p>Perform leading segment merge.
958962
*
959-
* <p>In some cases leading segment merge should be skipped, and this method will return false.
963+
* <p>
964+
* In some cases leading segment merge should be skipped, and this method will return false.
965+
* This is the case when:
966+
* - Leading segment merge is disabled through configuration
967+
* - There is a risk of integer overflow due to sparsity of the OnHeapGraph
968+
* - The OnHeapGraph is too sparse relative to it's size
960969
*
961970
* @return a boolean value indicating if leading segment merge was performed
962971
*/
963972
private boolean tryLeadingSegmentMerge() throws IOException {
973+
if (leadingSegmentMergeDisabled) {
974+
log.info("Leading segment merge is disabled, skipping");
975+
return false;
976+
}
977+
964978
var leadingFieldsReader = (PerFieldKnnVectorsFormat.FieldsReader) readers[LEADING_READER_IDX];
965979
var leadingReader = (JVectorReader) leadingFieldsReader.getFieldReader(fieldInfo.name);
966980
var graphReader = leadingReader.getNeighborsScoreCacheForField(fieldInfo.name);

src/main/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParams.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class KNNVectorsFormatParams {
2626
private boolean hierarchyEnabled;
2727
private Function<Integer, Integer> numberOfSubspacesPerVectorSupplier;
2828
private final SpaceType spaceType;
29+
private boolean leadingSegmentMergeDisabled;
2930

3031
public KNNVectorsFormatParams(final Map<String, Object> params, int defaultMaxConnections, int defaultBeamWidth) {
3132
this(
@@ -58,6 +59,7 @@ public KNNVectorsFormatParams(
5859
initHierarchyEnabled(params, defaultHierarchyEnabled);
5960
initNumberOfSubspacesPerVectorSupplier(params);
6061
this.spaceType = spaceType;
62+
initLeadingSegmentMergeDisabled(params, KNNConstants.DEFAULT_LEADING_SEGMENT_MERGE_DISABLED);
6163
}
6264

6365
public boolean validate(final Map<String, Object> params) {
@@ -120,4 +122,12 @@ private void initNumberOfSubspacesPerVectorSupplier(final Map<String, Object> pa
120122
}
121123
this.numberOfSubspacesPerVectorSupplier = JVectorFormat::getDefaultNumberOfSubspacesPerVector;
122124
}
125+
126+
private void initLeadingSegmentMergeDisabled(final Map<String, Object> params, boolean defaultLsmDisabled) {
127+
if (params != null && params.containsKey(KNNConstants.METHOD_PARAMETER_LEADING_SEGMENT_MERGE_DISABLED)) {
128+
this.hierarchyEnabled = (boolean) params.get(KNNConstants.METHOD_PARAMETER_LEADING_SEGMENT_MERGE_DISABLED);
129+
return;
130+
}
131+
this.leadingSegmentMergeDisabled = defaultLsmDisabled;
132+
}
123133
}

src/test/java/org/opensearch/knn/index/codec/jvector/JVectorWriterMergeTests.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ static class MergeTestScenario {
8585
int overqueryFactor = KNNConstants.DEFAULT_OVER_QUERY_FACTOR;
8686
@Default
8787
double minimumRecall = 0.99;
88+
@Default
89+
boolean leadingSegmentMergeDisabled = KNNConstants.DEFAULT_LEADING_SEGMENT_MERGE_DISABLED;
8890
}
8991

9092
@Rule
@@ -102,7 +104,7 @@ void runScenario(MergeTestScenario scenario) throws IOException {
102104
// Path indexPath = createTempDir();
103105
IndexWriterConfig iwc = LuceneTestCase.newIndexWriterConfig();
104106
iwc.setUseCompoundFile(false);
105-
iwc.setCodec(getCodec(scenario.minPqThreshold));
107+
iwc.setCodec(getCodec(scenario.minPqThreshold, scenario.leadingSegmentMergeDisabled));
106108
iwc.setMergePolicy(new ForceMergesOnlyMergePolicy(false));
107109

108110
try (var fsd = FSDirectory.open(tempDir.getRoot().toPath()); var writer = new IndexWriter(fsd, iwc);) {
@@ -350,8 +352,9 @@ public void multiPhaseMergeWithDeletesProgressivePQ() throws IOException {
350352
.deletionRanges(List.of(new DeletionRange(998, 1023), new DeletionRange(1023, 1100), new DeletionRange(2502, 2504)))
351353
.build()
352354
)
355+
.round(MergeTestRound.builder().segmentSizes(List.of(400)).build()) // an extra round just because
353356
.overqueryFactor(20)
354-
.minimumRecall(0.99)
357+
.minimumRecall(0.98)
355358
.build();
356359
runScenario(scenario);
357360
}
@@ -390,4 +393,50 @@ public void multiPhaseMergeWithDeletesAlwaysPQ() throws IOException {
390393
.build();
391394
runScenario(scenario);
392395
}
396+
397+
@Test
398+
public void testLeadingSegmentMergeDisabled() throws IOException {
399+
// we'll progressively increase to beyond the PQ threshold
400+
var scenario = MergeTestScenario.builder()
401+
.minPqThreshold(1000) // start using PQ once the vector count crosses this
402+
.leadingSegmentMergeDisabled(true) // and disable leading segment merge
403+
.round(
404+
MergeTestRound.builder()
405+
.segmentSizes(List.of(10, 200, 50, 100, 100)) // count 460
406+
.deletionRanges(
407+
List.of(
408+
// total deletions 126
409+
new DeletionRange(0, 5), // count 5
410+
new DeletionRange(210, 260), // count 50
411+
new DeletionRange(330, 400), // count 70
412+
new DeletionRange(410, 411) // count 1
413+
)
414+
)
415+
.build() // count 334
416+
)
417+
.round(
418+
MergeTestRound.builder()
419+
.segmentSizes(List.of(10, 20, 300)) // count 330
420+
.deletionRanges(
421+
List.of(
422+
// total deletions 195
423+
new DeletionRange(405, 600), // count 195, BUT this range has some overlap so actually 194
424+
new DeletionRange(10, 11) // count 1
425+
)
426+
)
427+
.build() // +135, total count 469
428+
)
429+
.round(MergeTestRound.builder().segmentSizes(List.of(50, 200)).build()) // bonus round
430+
.round(
431+
MergeTestRound.builder()
432+
.segmentSizes(List.of(20, 2000, 1)) // Add 2000 vectors to ensure PQ
433+
.deletionRanges(List.of(new DeletionRange(998, 1023), new DeletionRange(1023, 1100), new DeletionRange(2502, 2504)))
434+
.build()
435+
)
436+
.round(MergeTestRound.builder().segmentSizes(List.of(400)).build())
437+
.overqueryFactor(20)
438+
.minimumRecall(0.99)
439+
.build();
440+
runScenario(scenario);
441+
}
393442
}

src/test/java/org/opensearch/knn/index/engine/CommonTestUtils.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import static org.opensearch.knn.common.KNNConstants.DISK_ANN;
5050
import static org.opensearch.knn.common.KNNConstants.VECTOR_DATA_TYPE_FIELD;
5151
import static org.opensearch.knn.index.KNNSettings.KNN_INDEX;
52+
import static org.opensearch.knn.common.KNNConstants.DEFAULT_LEADING_SEGMENT_MERGE_DISABLED;
5253
import static org.opensearch.knn.common.KNNConstants.DEFAULT_MINIMUM_BATCH_SIZE_FOR_QUANTIZATION;
5354

5455
public class CommonTestUtils {
@@ -123,18 +124,22 @@ public static String createIndexMapping(int dimension, SpaceType spaceType, Vect
123124
}
124125

125126
public static Codec getCodec() {
126-
return getCodec(DEFAULT_MINIMUM_BATCH_SIZE_FOR_QUANTIZATION);
127+
return getCodec(DEFAULT_MINIMUM_BATCH_SIZE_FOR_QUANTIZATION, DEFAULT_LEADING_SEGMENT_MERGE_DISABLED);
127128
}
128129

129130
public static Codec getCodec(int minBatchSizeForQuantization) {
131+
return getCodec(minBatchSizeForQuantization, DEFAULT_LEADING_SEGMENT_MERGE_DISABLED);
132+
}
133+
134+
public static Codec getCodec(int minBatchSizeForQuantization, boolean leadingSegmentMergeDisabled) {
130135
return new FilterCodec(KNNCodecVersion.V_10_03_0.getCodecName(), new Lucene103Codec()) {
131136
@Override
132137
public KnnVectorsFormat knnVectorsFormat() {
133138
return new PerFieldKnnVectorsFormat() {
134139

135140
@Override
136141
public KnnVectorsFormat getKnnVectorsFormatForField(String field) {
137-
return new JVectorFormat(minBatchSizeForQuantization);
142+
return new JVectorFormat(minBatchSizeForQuantization, leadingSegmentMergeDisabled);
138143
}
139144
};
140145
}

src/test/java/org/opensearch/knn/index/engine/JVectorEngineIT.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public void testAddDoc() throws Exception {
104104
.field(KNNConstants.METHOD_PARAMETER_NEIGHBOR_OVERFLOW, 2.0)
105105
.field(KNNConstants.METHOD_PARAMETER_HIERARCHY_ENABLED, false)
106106
.field(KNNConstants.METHOD_PARAMETER_MIN_BATCH_SIZE_FOR_QUANTIZATION, 1000)
107+
.field(KNNConstants.METHOD_PARAMETER_LEADING_SEGMENT_MERGE_DISABLED, false)
107108
.endObject()
108109
.endObject()
109110
.endObject()

0 commit comments

Comments
 (0)