Skip to content

Commit 998e932

Browse files
authored
increment version of jVector to support incremental construction (#167)
persist neighbors cache add support for sorted index searcher fixes for resolving the node -> docId add incremental merge construction with leading segment move additional tests to internal test for transparency update documentation add readme pictures remove doc values by default separate docIdtoOrdMap class and add tests Signed-off-by: Samuel Herman <sherman8915@gmail.com>
1 parent f4cbfd9 commit 998e932

32 files changed

Lines changed: 1556 additions & 496 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
### Features
99
### Enhancements
1010
* PQ refinement during merge [109](https://github.com/opensearch-project/opensearch-jvector/issues/109)
11+
* Persistent Ordinal To docID Mapping [167](https://github.com/opensearch-project/opensearch-jvector/pull/167)
12+
* Incremental Insertion With Leading Segment [167](https://github.com/opensearch-project/opensearch-jvector/pull/167)
13+
* Remove Redundant FlatVectorFormat [167](https://github.com/opensearch-project/opensearch-jvector/pull/167)
14+
* Remove Redundant DocValuesFormat [167](https://github.com/opensearch-project/opensearch-jvector/pull/167)
1115
### Bug Fixes
16+
* Fix for sorted indices [167](https://github.com/opensearch-project/opensearch-jvector/pull/167)
17+
* Fix for missing fields [167](https://github.com/opensearch-project/opensearch-jvector/pull/167)
1218
### Infrastructure
1319
### Documentation
1420
### Maintenance
1521
### Refactoring
22+
* Remove jVector Codec [167](https://github.com/opensearch-project/opensearch-jvector/pull/167)
1623

1724
## [Unreleased 2.x](https://github.com/opensearch-project/opensearch-jvector/compare/2.18...2.x)
1825
### Features

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
- _**DiskANN**_ - JVector is a pure Java implementation capable to perform vector ANN search in a way that is optimized for RAM bound environments with minimal additional overhead. No need involving native dependencies (FAISS) and cumbersome JNI mechanism.
2828
- _**Thread Safety**_ - JVector is a threadsafe index that supports concurrent modification and inserts with near perfect scalability as you add cores, Lucene is not threadsafe; This allows us to ingest much higher volume of vectors a lot faster without unnecessary merge operations to parallelize ingestion concurrency.
2929
- _**quantized index construction**_ - JVector can perform index construction w/ quantized vectors, saving memory = larger segments = fewer segments = faster searches
30+
- _**Quantization refinement**_ - JVector can refine the quantization codebooks during merge, this allows us to have a more accurate quantization and better recall without the penalty of full codebook recomputation
31+
- _**Incremental merges**_ - JVector plugin can perform incremental vector inserts into previously persisted indexes without the need for a full rebuild of the index. This is an enormous saving for updates (especially updates of large graphs!) which are a common operation in many search applications.
3032
- _**Quantized Disk ANN**_ - JVector supports DiskANN style quantization with rerank, it's quite easy (in principle) to demonstrate that this is a massive difference in performance for larger-than-memory indexes (in practice it takes days/weeks to insert enough vectors into Lucene to show this b/c of the single threaded problem, that's the only hard part)
3133
- _**PQ and BQ support**_ - As part of (3) JVector supports PQ as well as the BQ that Lucene offers, it seems that this is fairly rare (pgvector doesn't do PQ either) because (1) the code required to get high performance ADC with SIMD is a bit involved and (2) it requires a separate codebook which Lucene isn't set up to easily accommodate. PQ at 64x compression gives you higher relevance than BQ at 32x
3234
- _**Fused ADC**_ - Features that nobody else has like Fused ADC and NVQ and Anisotropic PQ
@@ -132,6 +134,19 @@ For example if Lucene is doing 100x the number of disk reads compared to JVector
132134
![latency.png](latency.png)
133135
![recall.png](recall.png)
134136

137+
### Incremental merges
138+
Incremental merges are a key differentiator between JVector and other KNN engines. The following graphs show the cost of merges as a function of the number of documents.
139+
You can see the stark differences when we leverage the incremental merge capabilities of JVector plugin compared to the full rebuild scenario on every merge.
140+
141+
Without incremental merges:
142+
![merge_times_before_incremental.png](merge_times_before_incremental_smoothed_plot.png)
143+
144+
With incremental merges:
145+
![merge_times_after_incremental.png](merge_times_plot_after_incremental.png)
146+
147+
Comparison:
148+
![merge_times_comparison.png](merge_times_comparison.png)
149+
135150
## Credits and Acknowledgments
136151

137152
This project uses two similarity search libraries to perform Approximate Nearest Neighbor Search: the Apache 2.0-licensed [Lucene](https://github.com/apache/lucene) and [jVector](https://github.com/jbellis/jvector).

benchmark-jmh/src/jmh/java/org/opensearch/knn/index/codec/jvector/BenchmarkCommon.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat;
1212
import org.apache.lucene.index.VectorSimilarityFunction;
1313
import org.apache.lucene.search.TopDocs;
14+
import org.opensearch.knn.common.KNNConstants;
1415
import org.opensearch.knn.index.codec.KNNCodecVersion;
1516

1617
import java.util.PriorityQueue;
1718

18-
import static org.opensearch.knn.index.codec.jvector.JVectorFormat.DEFAULT_MINIMUM_BATCH_SIZE_FOR_QUANTIZATION;
19-
2019
public class BenchmarkCommon {
2120
public static final String JVECTOR_NOT_QUANTIZED = "jvector_not_quantized";
2221
public static final String JVECTOR_QUANTIZED = "jvector_quantized";
@@ -26,7 +25,7 @@ public class BenchmarkCommon {
2625
public static Codec getCodec(String codecType) {
2726
return switch (codecType) {
2827
case JVECTOR_NOT_QUANTIZED -> getFilterJvectorCodec(Integer.MAX_VALUE);
29-
case JVECTOR_QUANTIZED -> getFilterJvectorCodec(DEFAULT_MINIMUM_BATCH_SIZE_FOR_QUANTIZATION);
28+
case JVECTOR_QUANTIZED -> getFilterJvectorCodec(KNNConstants.DEFAULT_MINIMUM_BATCH_SIZE_FOR_QUANTIZATION);
3029
case LUCENE101 -> new Lucene101Codec();
3130
default -> throw new IllegalStateException("Unexpected codec type: " + codecType);
3231
};
@@ -84,7 +83,7 @@ public KnnVectorsFormat knnVectorsFormat() {
8483

8584
@Override
8685
public KnnVectorsFormat getKnnVectorsFormatForField(String field) {
87-
return new JVectorFormat(minBatchSizeForQuantization, true);
86+
return new JVectorFormat(minBatchSizeForQuantization);
8887
}
8988
};
9089
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
version=1.0.0
77
systemProp.bwc.version=1.3.4
8-
jvector_version=4.0.0-rc.2
8+
jvector_version=4.0.0-rc.4
99
java_release_version=21
1010

1111
# org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
45.1 KB
Loading

merge_times_comparison.png

138 KB
Loading
41.5 KB
Loading

scripts/README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ It's recommended to use a virtual environment to avoid conflicts with other Pyth
1717

1818
1. Create a virtual environment:
1919
```bash
20+
sudo apt install python3.11-venv
2021
# Using venv (Python 3.3+)
21-
python -m venv .venv
22+
python3 -m venv .venv
2223

2324
# Activate the virtual environment
2425
# On Windows:
@@ -138,3 +139,28 @@ python create_and_test_large_index.py --batch-size 1000 --force-merge-frequency
138139
# Generate plots from existing CSV
139140
python create_and_test_large_index.py --csv-output merge_times.csv --plot
140141
```
142+
143+
#### Important Note For Large Indices
144+
145+
When working with large indices, it's important to consider the point at which we will require quantization.
146+
Quantization is becoming critical during index construction when we can't fit the full precision vectors in memory and are forced to use disk.
147+
Therefore, we want to set the `minimum_batch_size_for_quantization` to a value high enough so we can avoid quantization during index construction.
148+
Or alternatively, we can set it to a lower value and accept the additional compute cost of quantization during index construction, and thus avoid the disk access.
149+
150+
```shell
151+
# Run with quantization disabled during index construction until we reach 10M documents
152+
python create_and_test_large_index.py --batch-size 1000 --force-merge-frequency 1000 --num-vectors 100000 --min-batch-size-for-quantization 10000000
153+
```
154+
155+
For long running tests you would want to move the script to run in the background and redirect the output to a file:
156+
```shell
157+
nohup python create_and_test_large_index.py --batch-size 5000 --force-merge-frequency 100000 --num-vectors 10000000 --min-batch-size-for-quantization 10000000 > output.log 2>&1 &
158+
```
159+
160+
You can also profile the java process while running the script:
161+
```shell
162+
# Get the process id of the opensearch java process
163+
PID=$(jps | grep OpenSearch | awk '{print $1}')
164+
# Start profiling
165+
jcmd $PID JFR.start name=OnDemand settings=profile duration=600s filename=/tmp/app_jfr_$(date +%s).jfr
166+
```

scripts/create_and_test_large_index.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
import matplotlib.pyplot as plt
1212
import os
1313

14-
def create_index(host, index_name, dimension, shards=1):
14+
def create_index(host, index_name, dimension, shards=1, min_batch_size_for_quantization=1000000):
1515
"""Create a knn index with jvector engine"""
1616
url = f"http://{host}/{index_name}"
1717

1818
mapping = {
1919
"settings": {
2020
"index": {
2121
"knn": True,
22+
"knn.derived_source.enabled": True,
2223
"number_of_shards": shards,
2324
"number_of_replicas": 0
2425
}
@@ -32,7 +33,9 @@ def create_index(host, index_name, dimension, shards=1):
3233
"name": "disk_ann",
3334
"space_type": "l2",
3435
"engine": "jvector",
35-
"parameters": {}
36+
"parameters": {
37+
"advanced.min_batch_size_for_quantization": min_batch_size_for_quantization
38+
}
3639
}
3740
},
3841
"id": {"type": "keyword"}
@@ -422,6 +425,8 @@ def main():
422425
help="Force merge after every N documents (0 to disable intermediate merges)")
423426
parser.add_argument("--csv-output", type=str, help="CSV file to save merge time data")
424427
parser.add_argument("--plot", action="store_true", help="Generate plots from CSV data")
428+
parser.add_argument("--min-batch-size-for-quantization", type=int, default=1000000,
429+
help="Minimum batch size for quantization (default: 1M)")
425430

426431
args = parser.parse_args()
427432

@@ -434,7 +439,7 @@ def main():
434439
print(f"Estimated size: ~{args.num_vectors * args.dimension * 4 / (1024*1024*1024):.2f} GB (raw vectors only)")
435440

436441
# Create index
437-
create_index(args.host, args.index, args.dimension, args.shards)
442+
create_index(args.host, args.index, args.dimension, args.shards, args.min_batch_size_for_quantization)
438443

439444
# Index vectors
440445
index_vectors(args.host, args.index, args.num_vectors, args.dimension, args.batch_size, args.force_merge_frequency, args.csv_output)
@@ -467,4 +472,4 @@ def main():
467472
plot_merge_times(args.csv_output)
468473

469474
if __name__ == "__main__":
470-
main()
475+
main()

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,14 @@ public class KNNConstants {
124124
// Construction related params
125125
public static final String METHOD_PARAMETER_ALPHA = "advanced.alpha";
126126
public static final String METHOD_PARAMETER_NEIGHBOR_OVERFLOW = "advanced.neighbor_overflow";
127-
public static final String METHOD_PARAMETER_HIERARCHY_ENABLED = "hierarchy_enabled"; // TODO: wire this after jvector upgrade
127+
public static final String METHOD_PARAMETER_MIN_BATCH_SIZE_FOR_QUANTIZATION = "advanced.min_batch_size_for_quantization";
128+
public static final String METHOD_PARAMETER_HIERARCHY_ENABLED = "advanced.hierarchy_enabled";
128129
public static final String METHOD_PARAMETER_NUM_PQ_SUBSPACES = "advanced.num_pq_subspaces";
129130
public static final Double DEFAULT_ALPHA_VALUE = 1.2;
130131
public static final Double DEFAULT_NEIGHBOR_OVERFLOW_VALUE = 1.2;
131-
public static final Boolean DEFAULT_HIERARCHY_ENABLED = true; // TODO: wire this after jvector upgrade
132+
public static final int DEFAULT_MINIMUM_BATCH_SIZE_FOR_QUANTIZATION = 1024; // above this batch size we will trigger quantization by
133+
// default
134+
public static final Boolean DEFAULT_HIERARCHY_ENABLED = false;
132135

133136
// Parameter defaults/limits
134137
public static final Integer ENCODER_PARAMETER_PQ_CODE_COUNT_DEFAULT = 1;

0 commit comments

Comments
 (0)