Skip to content

Commit f854146

Browse files
feat(oss-opensearch): Add Disk-based vector search support
1 parent 3e6c993 commit f854146

4 files changed

Lines changed: 205 additions & 21 deletions

File tree

vectordb_bench/backend/clients/oss_opensearch/config.py

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,30 @@ class OSSOpenSearchQuantization(Enum):
5555
fp16 = "fp16"
5656

5757

58+
# Compression level constants for disk-based mode
59+
class CompressionLevel:
60+
"""Valid compression levels for disk-based vector search"""
61+
62+
LEVEL_1X = "1x"
63+
LEVEL_2X = "2x"
64+
LEVEL_4X = "4x"
65+
LEVEL_8X = "8x"
66+
LEVEL_16X = "16x"
67+
LEVEL_32X = "32x"
68+
69+
ALL = [LEVEL_1X, LEVEL_2X, LEVEL_4X, LEVEL_8X, LEVEL_16X, LEVEL_32X]
70+
71+
# Lucene: 1x, 4x | FAISS: 2x, 8x, 16x, 32x
72+
ENGINE_MAP = {
73+
LEVEL_1X: OSSOS_Engine.lucene,
74+
LEVEL_2X: OSSOS_Engine.faiss,
75+
LEVEL_4X: OSSOS_Engine.lucene,
76+
LEVEL_8X: OSSOS_Engine.faiss,
77+
LEVEL_16X: OSSOS_Engine.faiss,
78+
LEVEL_32X: OSSOS_Engine.faiss,
79+
}
80+
81+
5882
class OSSOpenSearchIndexConfig(BaseModel, DBCaseConfig):
5983
metric_type: MetricType = MetricType.L2
6084
engine: OSSOS_Engine = OSSOS_Engine.faiss
@@ -74,11 +98,13 @@ class OSSOpenSearchIndexConfig(BaseModel, DBCaseConfig):
7498
cb_threshold: str | None = "50%"
7599
number_of_indexing_clients: int | None = 1
76100
use_routing: bool = False # for label-filter cases
77-
oversample_factor: float = 1.0
78101
quantization_type: OSSOpenSearchQuantization = OSSOpenSearchQuantization.fp32
79102
replication_type: str | None = "DOCUMENT"
80103
knn_derived_source_enabled: bool = False
81104
memory_optimized_search: bool = False
105+
on_disk: bool = False
106+
compression_level: str = CompressionLevel.LEVEL_32X
107+
oversample_factor: float = 1.0
82108

83109
@root_validator
84110
def validate_engine_name(cls, values: dict):
@@ -107,6 +133,9 @@ def __eq__(self, obj: any):
107133
and self.replication_type == obj.replication_type
108134
and self.knn_derived_source_enabled == obj.knn_derived_source_enabled
109135
and self.memory_optimized_search == obj.memory_optimized_search
136+
and self.on_disk == obj.on_disk
137+
and self.compression_level == obj.compression_level
138+
and self.oversample_factor == obj.oversample_factor
110139
)
111140

112141
def __hash__(self) -> int:
@@ -123,6 +152,9 @@ def __hash__(self) -> int:
123152
self.replication_type,
124153
self.knn_derived_source_enabled,
125154
self.memory_optimized_search,
155+
self.on_disk,
156+
self.compression_level,
157+
self.oversample_factor,
126158
)
127159
)
128160

@@ -140,27 +172,48 @@ def parse_metric(self) -> str:
140172

141173
@property
142174
def use_quant(self) -> bool:
143-
return self.quantization_type is not OSSOpenSearchQuantization.fp32
175+
"""Only use in-memory quantization when NOT in disk mode"""
176+
return not self.on_disk and self.quantization_type is not OSSOpenSearchQuantization.fp32
177+
178+
@property
179+
def resolved_engine(self) -> OSSOS_Engine:
180+
"""Return engine based on mode: auto-selected for disk, configured for in-memory."""
181+
if self.on_disk:
182+
return CompressionLevel.ENGINE_MAP.get(self.compression_level, OSSOS_Engine.faiss)
183+
return self.engine
144184

145185
def index_param(self) -> dict:
146-
log.info(f"Using engine: {self.engine} for index creation")
147-
log.info(f"Using metric_type: {self.metric_type_name} for index creation")
148-
log.info(f"Resulting space_type: {self.parse_metric()} for index creation")
186+
resolved_engine = self.resolved_engine
187+
space_type = self.parse_metric()
188+
189+
log.info(
190+
f"Index configuration - "
191+
f"mode: {'disk' if self.on_disk else 'in-memory'}, "
192+
f"configured_engine: {self.engine.value}, "
193+
f"resolved_engine: {resolved_engine.value}, "
194+
f"metric_type: {self.metric_type_name}, "
195+
f"space_type: {space_type}"
196+
f"{', ' if self.on_disk else ''}"
197+
f"{'compression_level: ' + self.compression_level if self.on_disk else ''}"
198+
)
149199

150-
return {
200+
method_config = {
151201
"name": "hnsw",
152-
"engine": self.engine.value,
153-
"space_type": self.parse_metric(),
202+
"engine": resolved_engine.value,
203+
"space_type": space_type,
154204
"parameters": {
155205
"ef_construction": self.efConstruction,
156206
"m": self.M,
157-
**(
158-
{"encoder": {"name": "sq", "parameters": {"type": self.quantization_type.value}}}
159-
if self.use_quant
160-
else {}
161-
),
162207
},
163208
}
164209

210+
if self.use_quant:
211+
method_config["parameters"]["encoder"] = {
212+
"name": "sq",
213+
"parameters": {"type": self.quantization_type.value},
214+
}
215+
216+
return method_config
217+
165218
def search_param(self) -> dict:
166219
return {"ef_search": self.efSearch}

vectordb_bench/backend/clients/oss_opensearch/oss_opensearch.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ def build_knn_query(
153153
if filter_clause:
154154
knn_config["filter"] = filter_clause
155155

156-
if self.case_config.use_quant:
156+
# Handle rescoring for both in-memory quantization and disk-based modes
157+
if self.case_config.use_quant or self.case_config.on_disk:
157158
knn_config["rescore"] = {"oversample_factor": self.case_config.oversample_factor}
158159

159160
return {"size": k, "query": {"knn": {self.vector_col_name: knn_config}}}
@@ -275,11 +276,44 @@ def _get_version_specific_settings(self, cluster_version: Version) -> dict:
275276
version_specific_settings[name] = value
276277
return version_specific_settings
277278

279+
def _build_vector_field_mapping(self) -> dict[str, Any]:
280+
"""Build vector field mapping configuration based on storage mode."""
281+
vector_field = {
282+
"type": "knn_vector",
283+
"dimension": self.dim,
284+
"method": self.case_config.index_param(),
285+
}
286+
287+
if self.case_config.on_disk:
288+
vector_field.update(
289+
{
290+
"space_type": self.case_config.parse_metric(),
291+
"data_type": "float",
292+
"mode": "on_disk",
293+
"compression_level": self.case_config.compression_level,
294+
}
295+
)
296+
log.info(
297+
f"Creating disk-based index - "
298+
f"compression_level: {self.case_config.compression_level}, "
299+
f"resolved_engine: {self.case_config.resolved_engine.value}"
300+
)
301+
else:
302+
log.info(f"Creating in-memory index with engine: {self.case_config.engine.value}")
303+
304+
return vector_field
305+
278306
def _get_bulk_manager(self, client: OpenSearch) -> BulkInsertManager:
279307
"""Get bulk insert manager for the given client."""
280308
return BulkInsertManager(client, self.index_name, self.case_config)
281309

282310
def _create_index(self, client: OpenSearch) -> None:
311+
cluster_version = self._get_cluster_version(client)
312+
313+
if self.case_config.on_disk and cluster_version < Version("2.17"):
314+
error_msg = f"Disk-based vector search requires OpenSearch 2.17+, but cluster is running {cluster_version}"
315+
raise OpenSearchError(error_msg)
316+
283317
ef_search_value = self.case_config.efSearch
284318
log.info(f"Creating index with ef_search: {ef_search_value}")
285319
log.info(f"Creating index with number_of_replicas: {self.case_config.number_of_replicas}")
@@ -324,11 +358,7 @@ def _create_index(self, client: OpenSearch) -> None:
324358
properties[self.id_col_name] = {"type": "integer", "store": True}
325359

326360
properties[self.label_col_name] = {"type": "keyword"}
327-
properties[self.vector_col_name] = {
328-
"type": "knn_vector",
329-
"dimension": self.dim,
330-
"method": self.case_config.index_param(),
331-
}
361+
properties[self.vector_col_name] = self._build_vector_field_mapping()
332362

333363
mappings = {
334364
"properties": properties,

vectordb_bench/frontend/config/dbCaseConfigs.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1903,6 +1903,65 @@ class CaseConfigInput(BaseModel):
19031903
isDisplayed=lambda config: (config.get(CaseConfigParamType.engine_name, "").lower() == "faiss"),
19041904
)
19051905

1906+
CaseConfigParamInput_ON_DISK_OSSOpensearch = CaseConfigInput(
1907+
label=CaseConfigParamType.on_disk,
1908+
displayLabel="Disk-based Search",
1909+
inputHelp="Enable disk-based storage with Binary Quantization",
1910+
inputType=InputType.Bool,
1911+
inputConfig={
1912+
"value": False,
1913+
},
1914+
)
1915+
1916+
CaseConfigParamInput_COMPRESSION_LEVEL_OSSOpensearch = CaseConfigInput(
1917+
label=CaseConfigParamType.compression_level,
1918+
displayLabel="Compression Level",
1919+
inputHelp="Binary quantization compression ratio for disk storage",
1920+
inputType=InputType.Option,
1921+
inputConfig={
1922+
"options": ["32x", "16x", "8x", "4x", "2x", "1x"],
1923+
"default": "32x",
1924+
},
1925+
isDisplayed=lambda config: config.get(CaseConfigParamType.on_disk, False) == True,
1926+
)
1927+
1928+
CaseConfigParamInput_OVERSAMPLE_FACTOR_OSSOpensearch = CaseConfigInput(
1929+
label=CaseConfigParamType.oversample_factor,
1930+
displayLabel="Oversample Factor",
1931+
inputHelp="Rescoring oversample factor for two-phase search",
1932+
inputType=InputType.Float,
1933+
inputConfig={
1934+
"min": 1.0,
1935+
"max": 10.0,
1936+
"value": 3.0,
1937+
"step": 0.5,
1938+
},
1939+
isDisplayed=lambda config: config.get(CaseConfigParamType.on_disk, False) == True,
1940+
)
1941+
1942+
CaseConfigParamInput_ENGINE_NAME_OSSOpensearch = CaseConfigInput(
1943+
label=CaseConfigParamType.engine_name,
1944+
displayLabel="Engine",
1945+
inputHelp="HNSW algorithm implementation to use",
1946+
inputType=InputType.Option,
1947+
inputConfig={
1948+
"options": ["faiss", "lucene"],
1949+
"default": "faiss",
1950+
},
1951+
isDisplayed=lambda config: config.get(CaseConfigParamType.on_disk, False) == False,
1952+
)
1953+
1954+
CaseConfigParamInput_QUANTIZATION_TYPE_OSSOpensearch = CaseConfigInput(
1955+
label=CaseConfigParamType.quantizationType,
1956+
displayLabel="Quantization Type",
1957+
inputHelp="Scalar quantization type for in-memory vectors",
1958+
inputType=InputType.Option,
1959+
inputConfig={
1960+
"options": ["fp32", "fp16"],
1961+
"default": "fp32",
1962+
},
1963+
isDisplayed=lambda config: config.get(CaseConfigParamType.on_disk, False) == False,
1964+
)
19061965
MilvusLoadConfig = [
19071966
CaseConfigParamInput_IndexType,
19081967
CaseConfigParamInput_M,
@@ -2356,6 +2415,45 @@ class CaseConfigInput(BaseModel):
23562415
CaseConfigParamInput_INDEX_THREAD_QTY_DURING_FORCE_MERGE_AWSOpensearch,
23572416
]
23582417

2418+
2419+
OSSOpensearchLoadingConfig = [
2420+
CaseConfigParamInput_ON_DISK_OSSOpensearch,
2421+
CaseConfigParamInput_COMPRESSION_LEVEL_OSSOpensearch,
2422+
CaseConfigParamInput_ENGINE_NAME_OSSOpensearch,
2423+
CaseConfigParamInput_METRIC_TYPE_NAME_AWSOpensearch,
2424+
CaseConfigParamInput_M_AWSOpensearch,
2425+
CaseConfigParamInput_EFConstruction_AWSOpensearch,
2426+
CaseConfigParamInput_QUANTIZATION_TYPE_OSSOpensearch,
2427+
CaseConfigParamInput_NUMBER_OF_SHARDS_AWSOpensearch,
2428+
CaseConfigParamInput_NUMBER_OF_REPLICAS_AWSOpensearch,
2429+
CaseConfigParamInput_REFRESH_INTERVAL_AWSOpensearch,
2430+
CaseConfigParamInput_NUMBER_OF_INDEXING_CLIENTS_AWSOpensearch,
2431+
CaseConfigParamInput_INDEX_THREAD_QTY_AWSOpensearch,
2432+
CaseConfigParamInput_REPLICATION_TYPE_AWSOpensearch,
2433+
CaseConfigParamInput_KNN_DERIVED_SOURCE_ENABLED_AWSOpensearch,
2434+
CaseConfigParamInput_MEMORY_OPTIMIZED_SEARCH_AWSOpensearch,
2435+
]
2436+
2437+
OSSOpenSearchPerformanceConfig = [
2438+
CaseConfigParamInput_ON_DISK_OSSOpensearch,
2439+
CaseConfigParamInput_COMPRESSION_LEVEL_OSSOpensearch,
2440+
CaseConfigParamInput_OVERSAMPLE_FACTOR_OSSOpensearch,
2441+
CaseConfigParamInput_EF_SEARCH_AWSOpensearch,
2442+
CaseConfigParamInput_ENGINE_NAME_OSSOpensearch,
2443+
CaseConfigParamInput_METRIC_TYPE_NAME_AWSOpensearch,
2444+
CaseConfigParamInput_M_AWSOpensearch,
2445+
CaseConfigParamInput_EFConstruction_AWSOpensearch,
2446+
CaseConfigParamInput_QUANTIZATION_TYPE_OSSOpensearch,
2447+
CaseConfigParamInput_NUMBER_OF_SHARDS_AWSOpensearch,
2448+
CaseConfigParamInput_NUMBER_OF_REPLICAS_AWSOpensearch,
2449+
CaseConfigParamInput_REFRESH_INTERVAL_AWSOpensearch,
2450+
CaseConfigParamInput_NUMBER_OF_INDEXING_CLIENTS_AWSOpensearch,
2451+
CaseConfigParamInput_INDEX_THREAD_QTY_AWSOpensearch,
2452+
CaseConfigParamInput_REPLICATION_TYPE_AWSOpensearch,
2453+
CaseConfigParamInput_KNN_DERIVED_SOURCE_ENABLED_AWSOpensearch,
2454+
CaseConfigParamInput_MEMORY_OPTIMIZED_SEARCH_AWSOpensearch,
2455+
]
2456+
23592457
# Map DB to config
23602458
CASE_CONFIG_MAP = {
23612459
DB.Milvus: {
@@ -2379,8 +2477,8 @@ class CaseConfigInput(BaseModel):
23792477
CaseLabel.Performance: AWSOpenSearchPerformanceConfig,
23802478
},
23812479
DB.OSSOpenSearch: {
2382-
CaseLabel.Load: AWSOpensearchLoadingConfig,
2383-
CaseLabel.Performance: AWSOpenSearchPerformanceConfig,
2480+
CaseLabel.Load: OSSOpensearchLoadingConfig,
2481+
CaseLabel.Performance: OSSOpenSearchPerformanceConfig,
23842482
},
23852483
DB.PgVector: {
23862484
CaseLabel.Load: PgVectorLoadingConfig,

vectordb_bench/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ class CaseConfigParamType(Enum):
129129
replication_type = "replication_type"
130130
knn_derived_source_enabled = "knn_derived_source_enabled"
131131
memory_optimized_search = "memory_optimized_search"
132+
on_disk = "on_disk"
133+
compression_level = "compression_level"
134+
oversample_factor = "oversample_factor"
132135

133136
# CockroachDB parameters
134137
min_partition_size = "min_partition_size"

0 commit comments

Comments
 (0)