@@ -697,6 +697,10 @@ class RandomAccessMergedFloatVectorValues implements RandomAccessVectorValues {
697697 // Ordinal sparsity has a memory cost (in terms map memory usage)
698698 // during leading segment merge.
699699 private static final double MIN_HEAP_GRAPH_ORDINAL_DENSITY = 0.4 ;
700+ // a number in [0.0, 1.0] that indicates how much leading segment live vectors dominate
701+ // over live vectors across all other segments, this is relevant when PQ is triggered since
702+ // only leading segment vectors will be taken into account
703+ private static final double MAX_PQ_LEADING_SEGMENT_LIVE_VECTOR_FACTOR = 0.1 ;
700704
701705 // Array of sub-readers
702706 private final KnnVectorsReader [] readers ;
@@ -1077,78 +1081,83 @@ private void mergePQ(RemappedRandomAccessVectorValues compactRavv) throws IOExce
10771081 PerFieldKnnVectorsFormat .FieldsReader fieldsReader = (PerFieldKnnVectorsFormat .FieldsReader ) readers [LEADING_READER_IDX ];
10781082 JVectorReader leadingReader = (JVectorReader ) fieldsReader .getFieldReader (fieldName );
10791083
1080- // Check if the leading reader has pre-existing PQ codebooks and if so, refine them with the remaining vectors
1081- if (leadingReader .getProductQuantizationForField (fieldInfo .name ).isEmpty ()) {
1082- // No pre-existing codebooks, check if we have enough vectors to trigger quantization
1083- log .info (
1084- "No Pre-existing PQ codebooks found in this merge for field {} in segment {}, will check if a new codebooks is necessary" ,
1085- fieldName ,
1086- mergeState .segmentInfo .name
1087- );
1088- if (totalLiveVectorsCount >= minimumBatchSizeForQuantization ) {
1084+ final ProductQuantization leadingCompressor ;
1085+ if (leadingReader .getProductQuantizationForField (fieldName ).isEmpty () == false ) {
1086+ final long start = Clock .systemDefaultZone ().millis ();
1087+ leadingCompressor = leadingReader .getProductQuantizationForField (fieldName ).get ();
1088+ final long end = Clock .systemDefaultZone ().millis ();
1089+ final long trainingTime = end - start ;
1090+ log .info ("Refined PQ codebooks for field {}, in {} millis" , fieldName , trainingTime );
1091+ KNNCounter .KNN_QUANTIZATION_TRAINING_TIME .add (trainingTime );
1092+ } else {
1093+ leadingCompressor = null ;
1094+ }
1095+
1096+ boolean ok = tryLeadingSegmentMerge (leadingCompressor );
1097+ if (!ok ) {
1098+ // Check if the leading reader has pre-existing PQ codebooks and if so, refine them with the remaining vectors
1099+ if (leadingCompressor == null ) {
1100+ // No pre-existing codebooks, check if we have enough vectors to trigger quantization
10891101 log .info (
1090- "Calculating new codebooks and compressed vectors for field: {}, with totalVectorCount: {}, above minimumBatchSizeForQuantization: {} " ,
1102+ "No Pre-existing PQ codebooks found in this merge for field {} in segment {}, will check if a new codebooks is necessary " ,
10911103 fieldName ,
1092- totalVectorsCount ,
1093- minimumBatchSizeForQuantization
1094- );
1095- compactPqVectors = JVectorIndexQuantization .computePqVectors (
1096- compactRavv ,
1097- getVectorSimilarityFunction (fieldInfo ),
1098- quantization .numSubspaces (compactRavv .dimension ()),
1099- simdPoolMerge
1104+ mergeState .segmentInfo .name
11001105 );
1106+ if (totalLiveVectorsCount >= minimumBatchSizeForQuantization ) {
1107+ log .info (
1108+ "Calculating new codebooks and compressed vectors for field: {}, with totalVectorCount: {}, above minimumBatchSizeForQuantization: {}" ,
1109+ fieldName ,
1110+ totalVectorsCount ,
1111+ minimumBatchSizeForQuantization
1112+ );
1113+ compactPqVectors = JVectorIndexQuantization .computePqVectors (
1114+ compactRavv ,
1115+ getVectorSimilarityFunction (fieldInfo ),
1116+ quantization .numSubspaces (compactRavv .dimension ()),
1117+ simdPoolMerge
1118+ );
1119+ } else {
1120+ log .info (
1121+ "Not enough vectors found for field: {}, totalVectorCount: {}, is below minimumBatchSizeForQuantization: {}" ,
1122+ fieldName ,
1123+ totalVectorsCount ,
1124+ minimumBatchSizeForQuantization
1125+ );
1126+ compactPqVectors = null ;
1127+ }
11011128 } else {
11021129 log .info (
1103- "Not enough vectors found for field: {}, totalVectorCount: {}, is below minimumBatchSizeForQuantization: {} " ,
1130+ "Pre-existing PQ codebooks found in this merge for field {} in segment {}, will refine the codebooks from the leading reader with the remaining vectors " ,
11041131 fieldName ,
1105- totalVectorsCount ,
1106- minimumBatchSizeForQuantization
1132+ mergeState .segmentInfo .name
11071133 );
1108- compactPqVectors = null ;
1134+ compactPqVectors = PQVectors . encodeAndBuild ( leadingCompressor , compactRavv . size (), compactRavv , simdPoolMerge ) ;
11091135 }
1110- } else {
1111- log .info (
1112- "Pre-existing PQ codebooks found in this merge for field {} in segment {}, will refine the codebooks from the leading reader with the remaining vectors" ,
1113- fieldName ,
1114- mergeState .segmentInfo .name
1115- );
1116- final long start = Clock .systemDefaultZone ().millis ();
1117- ProductQuantization leadingCompressor = leadingReader .getProductQuantizationForField (fieldName ).get ();
1118- // We are not refining PQ codes on merge presently.
1119- // See https://github.com/opensearch-project/opensearch-jvector/issues/661
1120- final long end = Clock .systemDefaultZone ().millis ();
1121- final long trainingTime = end - start ;
1122- log .info ("Refined PQ codebooks for field {}, in {} millis" , fieldName , trainingTime );
1123- KNNCounter .KNN_QUANTIZATION_TRAINING_TIME .add (trainingTime );
1124- compactPqVectors = PQVectors .encodeAndBuild (leadingCompressor , compactRavv .size (), compactRavv , simdPoolMerge );
1125- }
11261136
1127- if (compactPqVectors == null ) {
1128- final String segmentName = segmentWriteState .segmentInfo .name ;
1129- log .info ("No PQ codebooks found, will merge with full-precision vectors: field {} in segment {}" , fieldName , segmentName );
1130-
1131- boolean ok = tryLeadingSegmentMerge ();
1132- if (!ok ) {
1137+ if (compactPqVectors == null ) {
11331138 // leading segment merge was skipped
11341139 log .info (
11351140 "Merging segments by building graph from scratch (skipping leading segment merge) for segment {}, on field {}" ,
1136- segmentName ,
1141+ segmentWriteState . segmentInfo . name ,
11371142 fieldName
11381143 );
11391144 var bsp = BuildScoreProvider .randomAccessScoreProvider (compactRavv , getVectorSimilarityFunction (fieldInfo ));
11401145 var graph = getGraph (bsp , compactRavv , fieldInfo , segmentWriteState .segmentInfo .name , simdPoolMerge );
11411146 writeField (fieldInfo , compactRavv , compactOrdToDocMap , graph );
1147+ } else {
1148+ log .info ("PQ codebooks found, building graph from scratch with PQ vectors" );
1149+ // We're building from scratch, so we can use the "compact" ordinal space directly
1150+ var buildScoreProvider = BuildScoreProvider .pqBuildScoreProvider (
1151+ getVectorSimilarityFunction (fieldInfo ),
1152+ compactPqVectors
1153+ );
1154+ // Pre-init the diversity provider here to avoid doing it lazily (as it could block the SIMD threads)
1155+ buildScoreProvider .diversityProviderFor (0 );
1156+ var graph = getGraph (buildScoreProvider , compactRavv , fieldInfo , segmentWriteState .segmentInfo .name , simdPoolMerge );
1157+ writeField (fieldInfo , compactRavv , compactPqVectors , compactOrdToDocMap , graph );
11421158 }
1143- } else {
1144- log .info ("PQ codebooks found, building graph from scratch with PQ vectors" );
1145- // We're building from scratch, so we can use the "compact" ordinal space directly
1146- var buildScoreProvider = BuildScoreProvider .pqBuildScoreProvider (getVectorSimilarityFunction (fieldInfo ), compactPqVectors );
1147- // Pre-init the diversity provider here to avoid doing it lazily (as it could block the SIMD threads)
1148- buildScoreProvider .diversityProviderFor (0 );
1149- var graph = getGraph (buildScoreProvider , compactRavv , fieldInfo , segmentWriteState .segmentInfo .name , simdPoolMerge );
1150- writeField (fieldInfo , compactRavv , compactPqVectors , compactOrdToDocMap , graph );
11511159 }
1160+
11521161 }
11531162
11541163 /**
@@ -1163,7 +1172,7 @@ private void mergePQ(RemappedRandomAccessVectorValues compactRavv) throws IOExce
11631172 *
11641173 * @return a boolean value indicating if leading segment merge was performed
11651174 */
1166- private boolean tryLeadingSegmentMerge () throws IOException {
1175+ private boolean tryLeadingSegmentMerge (ProductQuantization leadingCompressor ) throws IOException {
11671176 if (leadingSegmentMergeDisabled ) {
11681177 log .info ("Leading segment merge is disabled, skipping" );
11691178 return false ;
@@ -1221,6 +1230,20 @@ private boolean tryLeadingSegmentMerge() throws IOException {
12211230 return false ;
12221231 }
12231232
1233+ if (leadingCompressor != null ) {
1234+ var leadingSegmentLiveVectorsFactor = totalLiveVectorsInOtherReaders / (double ) totalLiveVectorsInLeadingReader ;
1235+ if (leadingSegmentLiveVectorsFactor > MAX_PQ_LEADING_SEGMENT_LIVE_VECTOR_FACTOR ) {
1236+ log .warn (
1237+ "Leading segment does not contain sufficient live vectors to preserve the recall ({} / {}). "
1238+ + "Will skip leading segment merge. (totalLiveVectors={})" ,
1239+ totalLiveVectorsInOtherReaders ,
1240+ totalLiveVectorsInLeadingReader ,
1241+ totalLiveVectorsCount
1242+ );
1243+ return false ;
1244+ }
1245+ }
1246+
12241247 log .info (
12251248 "Starting leading segment merge for segment {} on field {}" ,
12261249 segmentWriteState .segmentInfo .name ,
@@ -1285,9 +1308,46 @@ private boolean tryLeadingSegmentMerge() throws IOException {
12851308 throw new IllegalStateException ("failed to fill one of the maps, this is a bug" );
12861309 }
12871310
1311+ PQVectors compactPqVectors = null ;
1312+ BuildScoreProvider leadingBsp = null ;
12881313 var heapRavv = new RemappedRandomAccessVectorValues (this , heapToGlobalRavvOrds );
1314+ if (leadingCompressor != null ) {
1315+ compactPqVectors = PQVectors .encodeAndBuild (leadingCompressor , heapRavv .size (), new RandomAccessVectorValues () {
1316+ @ Override
1317+ public int size () {
1318+ return heapRavv .size ();
1319+ }
12891320
1290- var leadingBsp = BuildScoreProvider .randomAccessScoreProvider (heapRavv , getVectorSimilarityFunction (fieldInfo ));
1321+ @ Override
1322+ public int dimension () {
1323+ return heapRavv .dimension ();
1324+ }
1325+
1326+ @ Override
1327+ public VectorFloat <?> getVector (int nodeId ) {
1328+ // PQVectors implementations does not deal with "holes" (deleted vectors)
1329+ final int remapped = heapToGlobalRavvOrds [nodeId ];
1330+ if (remapped == GraphNodeIdToDocMap .NO_VECTOR_OR_DELETED_DOC ) {
1331+ return null ; /* no vector */
1332+ } else {
1333+ return heapRavv .getVector (nodeId );
1334+ }
1335+ }
1336+
1337+ @ Override
1338+ public boolean isValueShared () {
1339+ return heapRavv .isValueShared ();
1340+ }
1341+
1342+ @ Override
1343+ public RandomAccessVectorValues copy () {
1344+ return heapRavv .copy ();
1345+ }
1346+ }, simdPoolMerge );
1347+ leadingBsp = BuildScoreProvider .pqBuildScoreProvider (getVectorSimilarityFunction (fieldInfo ), compactPqVectors );
1348+ } else {
1349+ leadingBsp = BuildScoreProvider .randomAccessScoreProvider (heapRavv , getVectorSimilarityFunction (fieldInfo ));
1350+ }
12911351
12921352 // we left this uninitialized earlier, but we're ready to set it up now
12931353 // just in time to mutate the graph
@@ -1334,7 +1394,11 @@ private boolean tryLeadingSegmentMerge() throws IOException {
13341394 // Note that the ordinals for the OnDiskGraphIndex will automatically be compacted
13351395 // But the OnHeapGraphIndex will not
13361396 var finalOrdToDocMap = new GraphNodeIdToDocMap (finalOrdToDocId );
1337- writeField (fieldInfo , heapRavv , finalOrdToDocMap , graph );
1397+ if (compactPqVectors != null ) {
1398+ writeField (fieldInfo , heapRavv , compactPqVectors , finalOrdToDocMap , graph );
1399+ } else {
1400+ writeField (fieldInfo , heapRavv , finalOrdToDocMap , graph );
1401+ }
13381402 return true ;
13391403 }
13401404 }
0 commit comments