vectordb-bench 0.0.16__py3-none-any.whl → 0.0.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ from abc import abstractmethod
2
+ from typing import Any, Mapping, Optional, Sequence, TypedDict
3
+ from pydantic import BaseModel, SecretStr
4
+ from typing_extensions import LiteralString
5
+ from ..api import DBCaseConfig, DBConfig, IndexType, MetricType
6
+
7
+ POSTGRE_URL_PLACEHOLDER = "postgresql://%s:%s@%s/%s"
8
+
9
+
10
+ class AlloyDBConfigDict(TypedDict):
11
+ """These keys will be directly used as kwargs in psycopg connection string,
12
+ so the names must match exactly psycopg API"""
13
+
14
+ user: str
15
+ password: str
16
+ host: str
17
+ port: int
18
+ dbname: str
19
+
20
+
21
+ class AlloyDBConfig(DBConfig):
22
+ user_name: SecretStr = SecretStr("postgres")
23
+ password: SecretStr
24
+ host: str = "localhost"
25
+ port: int = 5432
26
+ db_name: str
27
+
28
+ def to_dict(self) -> AlloyDBConfigDict:
29
+ user_str = self.user_name.get_secret_value()
30
+ pwd_str = self.password.get_secret_value()
31
+ return {
32
+ "host": self.host,
33
+ "port": self.port,
34
+ "dbname": self.db_name,
35
+ "user": user_str,
36
+ "password": pwd_str,
37
+ }
38
+
39
+
40
+ class AlloyDBIndexParam(TypedDict):
41
+ metric: str
42
+ index_type: str
43
+ index_creation_with_options: Sequence[dict[str, Any]]
44
+ maintenance_work_mem: Optional[str]
45
+ max_parallel_workers: Optional[int]
46
+
47
+
48
+ class AlloyDBSearchParam(TypedDict):
49
+ metric_fun_op: LiteralString
50
+
51
+
52
+ class AlloyDBSessionCommands(TypedDict):
53
+ session_options: Sequence[dict[str, Any]]
54
+
55
+
56
+ class AlloyDBIndexConfig(BaseModel, DBCaseConfig):
57
+ metric_type: MetricType | None = None
58
+ create_index_before_load: bool = False
59
+ create_index_after_load: bool = True
60
+
61
+ def parse_metric(self) -> str:
62
+ if self.metric_type == MetricType.L2:
63
+ return "l2"
64
+ elif self.metric_type == MetricType.DP:
65
+ return "dot_product"
66
+ return "cosine"
67
+
68
+ def parse_metric_fun_op(self) -> LiteralString:
69
+ if self.metric_type == MetricType.L2:
70
+ return "<->"
71
+ elif self.metric_type == MetricType.IP:
72
+ return "<#>"
73
+ return "<=>"
74
+
75
+ @abstractmethod
76
+ def index_param(self) -> AlloyDBIndexParam:
77
+ ...
78
+
79
+ @abstractmethod
80
+ def search_param(self) -> AlloyDBSearchParam:
81
+ ...
82
+
83
+ @abstractmethod
84
+ def session_param(self) -> AlloyDBSessionCommands:
85
+ ...
86
+
87
+ @staticmethod
88
+ def _optionally_build_with_options(with_options: Mapping[str, Any]) -> Sequence[dict[str, Any]]:
89
+ """Walk through mappings, creating a List of {key1 = value} pairs. That will be used to build a where clause"""
90
+ options = []
91
+ for option_name, value in with_options.items():
92
+ if value is not None:
93
+ options.append(
94
+ {
95
+ "option_name": option_name,
96
+ "val": str(value),
97
+ }
98
+ )
99
+ return options
100
+
101
+ @staticmethod
102
+ def _optionally_build_set_options(
103
+ set_mapping: Mapping[str, Any]
104
+ ) -> Sequence[dict[str, Any]]:
105
+ """Walk through options, creating 'SET 'key1 = "value1";' list"""
106
+ session_options = []
107
+ for setting_name, value in set_mapping.items():
108
+ if value:
109
+ session_options.append(
110
+ {"parameter": {
111
+ "setting_name": setting_name,
112
+ "val": str(value),
113
+ },
114
+ }
115
+ )
116
+ return session_options
117
+
118
+
119
+ class AlloyDBScaNNConfig(AlloyDBIndexConfig):
120
+ index: IndexType = IndexType.SCANN
121
+ num_leaves: int | None
122
+ quantizer: str | None
123
+ enable_pca: str | None
124
+ max_num_levels: int | None
125
+ num_leaves_to_search: int | None
126
+ max_top_neighbors_buffer_size: int | None
127
+ pre_reordering_num_neighbors: int | None
128
+ num_search_threads: int | None
129
+ max_num_prefetch_datasets: int | None
130
+ maintenance_work_mem: Optional[str] = None
131
+ max_parallel_workers: Optional[int] = None
132
+
133
+ def index_param(self) -> AlloyDBIndexParam:
134
+ index_parameters = {
135
+ "num_leaves": self.num_leaves, "max_num_levels": self.max_num_levels, "quantizer": self.quantizer,
136
+ }
137
+ return {
138
+ "metric": self.parse_metric(),
139
+ "index_type": self.index.value,
140
+ "index_creation_with_options": self._optionally_build_with_options(
141
+ index_parameters
142
+ ),
143
+ "maintenance_work_mem": self.maintenance_work_mem,
144
+ "max_parallel_workers": self.max_parallel_workers,
145
+ "enable_pca": self.enable_pca,
146
+ }
147
+
148
+ def search_param(self) -> AlloyDBSearchParam:
149
+ return {
150
+ "metric_fun_op": self.parse_metric_fun_op(),
151
+ }
152
+
153
+ def session_param(self) -> AlloyDBSessionCommands:
154
+ session_parameters = {
155
+ "scann.num_leaves_to_search": self.num_leaves_to_search,
156
+ "scann.max_top_neighbors_buffer_size": self.max_top_neighbors_buffer_size,
157
+ "scann.pre_reordering_num_neighbors": self.pre_reordering_num_neighbors,
158
+ "scann.num_search_threads": self.num_search_threads,
159
+ "scann.max_num_prefetch_datasets": self.max_num_prefetch_datasets,
160
+ }
161
+ return {
162
+ "session_options": self._optionally_build_set_options(session_parameters)
163
+ }
164
+
165
+
166
+ _alloydb_case_config = {
167
+ IndexType.SCANN: AlloyDBScaNNConfig,
168
+ }
@@ -10,6 +10,7 @@ class MetricType(str, Enum):
10
10
  L2 = "L2"
11
11
  COSINE = "COSINE"
12
12
  IP = "IP"
13
+ DP = "DP"
13
14
  HAMMING = "HAMMING"
14
15
  JACCARD = "JACCARD"
15
16
 
@@ -27,6 +28,7 @@ class IndexType(str, Enum):
27
28
  GPU_IVF_FLAT = "GPU_IVF_FLAT"
28
29
  GPU_IVF_PQ = "GPU_IVF_PQ"
29
30
  GPU_CAGRA = "GPU_CAGRA"
31
+ SCANN = "scann"
30
32
 
31
33
 
32
34
  class DBConfig(ABC, BaseModel):
@@ -66,7 +66,8 @@ class Milvus(VectorDB):
66
66
  self.case_config.index_param(),
67
67
  index_name=self._index_name,
68
68
  )
69
- # self._pre_load(coll)
69
+ if kwargs.get("pre_load") is True:
70
+ self._pre_load(col)
70
71
 
71
72
  connections.disconnect("default")
72
73
 
@@ -57,11 +57,11 @@ class CustomDataset(BaseDataset):
57
57
  dir: str
58
58
  file_num: int
59
59
  isCustom: bool = True
60
-
60
+
61
61
  @validator("size")
62
62
  def verify_size(cls, v):
63
63
  return v
64
-
64
+
65
65
  @property
66
66
  def label(self) -> str:
67
67
  return "Custom"
@@ -73,7 +73,8 @@ class CustomDataset(BaseDataset):
73
73
  @property
74
74
  def file_count(self) -> int:
75
75
  return self.file_num
76
-
76
+
77
+
77
78
  class LAION(BaseDataset):
78
79
  name: str = "LAION"
79
80
  dim: int = 768
@@ -242,13 +243,15 @@ class DataSetIterator:
242
243
  self._cur = None
243
244
  self._sub_idx = [0 for i in range(len(self._ds.train_files))] # iter num for each file
244
245
 
246
+ def __iter__(self):
247
+ return self
248
+
245
249
  def _get_iter(self, file_name: str):
246
250
  p = pathlib.Path(self._ds.data_dir, file_name)
247
251
  log.info(f"Get iterator for {p.name}")
248
252
  if not p.exists():
249
253
  raise IndexError(f"No such file {p}")
250
- log.warning(f"No such file: {p}")
251
- return ParquetFile(p).iter_batches(config.NUM_PER_BATCH)
254
+ return ParquetFile(p, memory_map=True, pre_buffer=True).iter_batches(config.NUM_PER_BATCH)
252
255
 
253
256
  def __next__(self) -> pd.DataFrame:
254
257
  """return the data in the next file of the training list"""
@@ -64,7 +64,7 @@ class MultiProcessingSearchRunner:
64
64
  log.warning(f"VectorDB search_embedding error: {e}")
65
65
  traceback.print_exc(chain=True)
66
66
  raise e from None
67
-
67
+
68
68
  latencies.append(time.perf_counter() - s)
69
69
  count += 1
70
70
  # loop through the test data
@@ -87,11 +87,14 @@ class MultiProcessingSearchRunner:
87
87
  log.debug(f"MultiProcessingSearchRunner get multiprocessing start method: {mp_start_method}")
88
88
  return mp.get_context(mp_start_method)
89
89
 
90
- def _run_all_concurrencies_mem_efficient(self) -> float:
90
+
91
+
92
+ def _run_all_concurrencies_mem_efficient(self):
91
93
  max_qps = 0
92
94
  conc_num_list = []
93
95
  conc_qps_list = []
94
96
  conc_latency_p99_list = []
97
+ conc_latency_avg_list = []
95
98
  try:
96
99
  for conc in self.concurrencies:
97
100
  with mp.Manager() as m:
@@ -111,13 +114,15 @@ class MultiProcessingSearchRunner:
111
114
  start = time.perf_counter()
112
115
  all_count = sum([r.result()[0] for r in future_iter])
113
116
  latencies = sum([r.result()[2] for r in future_iter], start=[])
114
- latency_p99 = np.percentile(latencies, 0.99)
117
+ latency_p99 = np.percentile(latencies, 99)
118
+ latency_avg = np.mean(latencies)
115
119
  cost = time.perf_counter() - start
116
120
 
117
121
  qps = round(all_count / cost, 4)
118
122
  conc_num_list.append(conc)
119
123
  conc_qps_list.append(qps)
120
124
  conc_latency_p99_list.append(latency_p99)
125
+ conc_latency_avg_list.append(latency_avg)
121
126
  log.info(f"End search in concurrency {conc}: dur={cost}s, total_count={all_count}, qps={qps}")
122
127
 
123
128
  if qps > max_qps:
@@ -134,7 +139,7 @@ class MultiProcessingSearchRunner:
134
139
  finally:
135
140
  self.stop()
136
141
 
137
- return max_qps, conc_num_list, conc_qps_list, conc_latency_p99_list
142
+ return max_qps, conc_num_list, conc_qps_list, conc_latency_p99_list, conc_latency_avg_list
138
143
 
139
144
  def run(self) -> float:
140
145
  """
@@ -145,3 +150,88 @@ class MultiProcessingSearchRunner:
145
150
 
146
151
  def stop(self) -> None:
147
152
  pass
153
+
154
+ def run_by_dur(self, duration: int) -> float:
155
+ return self._run_by_dur(duration)
156
+
157
+ def _run_by_dur(self, duration: int) -> float:
158
+ max_qps = 0
159
+ try:
160
+ for conc in self.concurrencies:
161
+ with mp.Manager() as m:
162
+ q, cond = m.Queue(), m.Condition()
163
+ with concurrent.futures.ProcessPoolExecutor(mp_context=self.get_mp_context(), max_workers=conc) as executor:
164
+ log.info(f"Start search_by_dur {duration}s in concurrency {conc}, filters: {self.filters}")
165
+ future_iter = [executor.submit(self.search_by_dur, duration, self.test_data, q, cond) for i in range(conc)]
166
+ # Sync all processes
167
+ while q.qsize() < conc:
168
+ sleep_t = conc if conc < 10 else 10
169
+ time.sleep(sleep_t)
170
+
171
+ with cond:
172
+ cond.notify_all()
173
+ log.info(f"Syncing all process and start concurrency search, concurrency={conc}")
174
+
175
+ start = time.perf_counter()
176
+ all_count = sum([r.result() for r in future_iter])
177
+ cost = time.perf_counter() - start
178
+
179
+ qps = round(all_count / cost, 4)
180
+ log.info(f"End search in concurrency {conc}: dur={cost}s, total_count={all_count}, qps={qps}")
181
+
182
+ if qps > max_qps:
183
+ max_qps = qps
184
+ log.info(f"Update largest qps with concurrency {conc}: current max_qps={max_qps}")
185
+ except Exception as e:
186
+ log.warning(f"Fail to search all concurrencies: {self.concurrencies}, max_qps before failure={max_qps}, reason={e}")
187
+ traceback.print_exc()
188
+
189
+ # No results available, raise exception
190
+ if max_qps == 0.0:
191
+ raise e from None
192
+
193
+ finally:
194
+ self.stop()
195
+
196
+ return max_qps
197
+
198
+
199
+ def search_by_dur(self, dur: int, test_data: list[list[float]], q: mp.Queue, cond: mp.Condition) -> int:
200
+ # sync all process
201
+ q.put(1)
202
+ with cond:
203
+ cond.wait()
204
+
205
+ with self.db.init():
206
+ num, idx = len(test_data), random.randint(0, len(test_data) - 1)
207
+
208
+ start_time = time.perf_counter()
209
+ count = 0
210
+ while time.perf_counter() < start_time + dur:
211
+ s = time.perf_counter()
212
+ try:
213
+ self.db.search_embedding(
214
+ test_data[idx],
215
+ self.k,
216
+ self.filters,
217
+ )
218
+ except Exception as e:
219
+ log.warning(f"VectorDB search_embedding error: {e}")
220
+ traceback.print_exc(chain=True)
221
+ raise e from None
222
+
223
+ count += 1
224
+ # loop through the test data
225
+ idx = idx + 1 if idx < num - 1 else 0
226
+
227
+ if count % 500 == 0:
228
+ log.debug(f"({mp.current_process().name:16}) search_count: {count}, latest_latency={time.perf_counter()-s}")
229
+
230
+ total_dur = round(time.perf_counter() - start_time, 4)
231
+ log.debug(
232
+ f"{mp.current_process().name:16} search {self.duration}s: "
233
+ f"actual_dur={total_dur}s, count={count}, qps in this process: {round(count / total_dur, 4):3}"
234
+ )
235
+
236
+ return count
237
+
@@ -0,0 +1,79 @@
1
+ import logging
2
+ import time
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ import multiprocessing as mp
5
+
6
+
7
+ from vectordb_bench.backend.clients import api
8
+ from vectordb_bench.backend.dataset import DataSetIterator
9
+ from vectordb_bench.backend.utils import time_it
10
+ from vectordb_bench import config
11
+
12
+ from .util import get_data, is_futures_completed, get_future_exceptions
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ class RatedMultiThreadingInsertRunner:
17
+ def __init__(
18
+ self,
19
+ rate: int, # numRows per second
20
+ db: api.VectorDB,
21
+ dataset_iter: DataSetIterator,
22
+ normalize: bool = False,
23
+ timeout: float | None = None,
24
+ ):
25
+ self.timeout = timeout if isinstance(timeout, (int, float)) else None
26
+ self.dataset = dataset_iter
27
+ self.db = db
28
+ self.normalize = normalize
29
+ self.insert_rate = rate
30
+ self.batch_rate = rate // config.NUM_PER_BATCH
31
+
32
+ def send_insert_task(self, db, emb: list[list[float]], metadata: list[str]):
33
+ db.insert_embeddings(emb, metadata)
34
+
35
+ @time_it
36
+ def run_with_rate(self, q: mp.Queue):
37
+ with ThreadPoolExecutor(max_workers=mp.cpu_count()) as executor:
38
+ executing_futures = []
39
+
40
+ @time_it
41
+ def submit_by_rate() -> bool:
42
+ rate = self.batch_rate
43
+ for data in self.dataset:
44
+ emb, metadata = get_data(data, self.normalize)
45
+ executing_futures.append(executor.submit(self.send_insert_task, self.db, emb, metadata))
46
+ rate -= 1
47
+
48
+ if rate == 0:
49
+ return False
50
+ return rate == self.batch_rate
51
+
52
+ with self.db.init():
53
+ while True:
54
+ start_time = time.perf_counter()
55
+ finished, elapsed_time = submit_by_rate()
56
+ if finished is True:
57
+ q.put(None, block=True)
58
+ log.info(f"End of dataset, left unfinished={len(executing_futures)}")
59
+ return
60
+
61
+ q.put(True, block=False)
62
+ wait_interval = 1 - elapsed_time if elapsed_time < 1 else 0.001
63
+
64
+ e, completed = is_futures_completed(executing_futures, wait_interval)
65
+ if completed is True:
66
+ ex = get_future_exceptions(executing_futures)
67
+ if ex is not None:
68
+ log.warn(f"task error, terminating, err={ex}")
69
+ q.put(None)
70
+ executor.shutdown(wait=True, cancel_futures=True)
71
+ raise ex
72
+ else:
73
+ log.debug(f"Finished {len(executing_futures)} insert-{config.NUM_PER_BATCH} task in 1s, wait_interval={wait_interval:.2f}")
74
+ executing_futures = []
75
+ else:
76
+ log.warning(f"Failed to finish tasks in 1s, {e}, waited={wait_interval:.2f}, try to check the next round")
77
+ dur = time.perf_counter() - start_time
78
+ if dur < 1:
79
+ time.sleep(1 - dur)
@@ -0,0 +1,112 @@
1
+ import logging
2
+ from typing import Iterable
3
+ import multiprocessing as mp
4
+ import concurrent
5
+ import numpy as np
6
+ import math
7
+
8
+ from .mp_runner import MultiProcessingSearchRunner
9
+ from .serial_runner import SerialSearchRunner
10
+ from .rate_runner import RatedMultiThreadingInsertRunner
11
+ from vectordb_bench.backend.clients import api
12
+ from vectordb_bench.backend.dataset import DatasetManager
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class ReadWriteRunner(MultiProcessingSearchRunner, RatedMultiThreadingInsertRunner):
18
+ def __init__(
19
+ self,
20
+ db: api.VectorDB,
21
+ dataset: DatasetManager,
22
+ insert_rate: int = 1000,
23
+ normalize: bool = False,
24
+ k: int = 100,
25
+ filters: dict | None = None,
26
+ concurrencies: Iterable[int] = (1, 15, 50),
27
+ search_stage: Iterable[float] = (0.5, 0.6, 0.7, 0.8, 0.9, 1.0), # search in any insert portion, 0.0 means search from the start
28
+ read_dur_after_write: int = 300, # seconds, search duration when insertion is done
29
+ timeout: float | None = None,
30
+ ):
31
+ self.insert_rate = insert_rate
32
+ self.data_volume = dataset.data.size
33
+
34
+ for stage in search_stage:
35
+ assert 0.0 <= stage <= 1.0, "each search stage should be in [0.0, 1.0]"
36
+ self.search_stage = sorted(search_stage)
37
+ self.read_dur_after_write = read_dur_after_write
38
+
39
+ log.info(f"Init runner, concurencys={concurrencies}, search_stage={search_stage}, stage_search_dur={read_dur_after_write}")
40
+
41
+ test_emb = np.stack(dataset.test_data["emb"])
42
+ if normalize:
43
+ test_emb = test_emb / np.linalg.norm(test_emb, axis=1)[:, np.newaxis]
44
+ test_emb = test_emb.tolist()
45
+
46
+ MultiProcessingSearchRunner.__init__(
47
+ self,
48
+ db=db,
49
+ test_data=test_emb,
50
+ k=k,
51
+ filters=filters,
52
+ concurrencies=concurrencies,
53
+ )
54
+ RatedMultiThreadingInsertRunner.__init__(
55
+ self,
56
+ rate=insert_rate,
57
+ db=db,
58
+ dataset_iter=iter(dataset),
59
+ normalize=normalize,
60
+ )
61
+ self.serial_search_runner = SerialSearchRunner(
62
+ db=db,
63
+ test_data=test_emb,
64
+ ground_truth=dataset.gt_data,
65
+ k=k,
66
+ )
67
+
68
+ def run_read_write(self):
69
+ futures = []
70
+ with mp.Manager() as m:
71
+ q = m.Queue()
72
+ with concurrent.futures.ProcessPoolExecutor(mp_context=mp.get_context("spawn"), max_workers=2) as executor:
73
+ futures.append(executor.submit(self.run_with_rate, q))
74
+ futures.append(executor.submit(self.run_search_by_sig, q))
75
+
76
+ for future in concurrent.futures.as_completed(futures):
77
+ res = future.result()
78
+ log.info(f"Result = {res}")
79
+
80
+ log.info("Concurrent read write all done")
81
+
82
+
83
+ def run_search_by_sig(self, q):
84
+ res = []
85
+ total_batch = math.ceil(self.data_volume / self.insert_rate)
86
+ batch = 0
87
+ recall = 'x'
88
+
89
+ for idx, stage in enumerate(self.search_stage):
90
+ target_batch = int(total_batch * stage)
91
+ while q.get(block=True):
92
+ batch += 1
93
+ if batch >= target_batch:
94
+ perc = int(stage * 100)
95
+ log.info(f"Insert {perc}% done, total batch={total_batch}")
96
+ log.info(f"[{batch}/{total_batch}] Serial search - {perc}% start")
97
+ recall, ndcg, p99 =self.serial_search_runner.run()
98
+
99
+ if idx < len(self.search_stage) - 1:
100
+ stage_search_dur = (self.data_volume * (self.search_stage[idx + 1] - stage) // self.insert_rate) // len(self.concurrencies)
101
+ if stage_search_dur < 30:
102
+ log.warning(f"Search duration too short, please reduce concurrency count or insert rate, or increase dataset volume: dur={stage_search_dur}, concurrencies={len(self.concurrencies)}, insert_rate={self.insert_rate}")
103
+ log.info(f"[{batch}/{total_batch}] Conc search - {perc}% start, dur for each conc={stage_search_dur}s")
104
+ else:
105
+ last_search_dur = self.data_volume * (1.0 - stage) // self.insert_rate
106
+ stage_search_dur = last_search_dur + self.read_dur_after_write
107
+ log.info(f"[{batch}/{total_batch}] Last conc search - {perc}% start, [read_until_write|read_after_write|total] =[{last_search_dur}s|{self.read_dur_after_write}s|{stage_search_dur}s]")
108
+
109
+ max_qps = self.run_by_dur(stage_search_dur)
110
+ res.append((perc, max_qps, recall))
111
+ break
112
+ return res
@@ -0,0 +1,32 @@
1
+ import logging
2
+ import concurrent
3
+ from typing import Iterable
4
+
5
+ from pandas import DataFrame
6
+ import numpy as np
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+ def get_data(data_df: DataFrame, normalize: bool) -> tuple[list[list[float]], list[str]]:
11
+ all_metadata = data_df['id'].tolist()
12
+ emb_np = np.stack(data_df['emb'])
13
+ if normalize:
14
+ log.debug("normalize the 100k train data")
15
+ all_embeddings = (emb_np / np.linalg.norm(emb_np, axis=1)[:, np.newaxis]).tolist()
16
+ else:
17
+ all_embeddings = emb_np.tolist()
18
+ return all_embeddings, all_metadata
19
+
20
+ def is_futures_completed(futures: Iterable[concurrent.futures.Future], interval) -> (Exception, bool):
21
+ try:
22
+ list(concurrent.futures.as_completed(futures, timeout=interval))
23
+ except TimeoutError as e:
24
+ return e, False
25
+ return None, True
26
+
27
+
28
+ def get_future_exceptions(futures: Iterable[concurrent.futures.Future]) -> BaseException | None:
29
+ for f in futures:
30
+ if f.exception() is not None:
31
+ return f.exception()
32
+ return
@@ -150,7 +150,7 @@ class CaseRunner(BaseModel):
150
150
  )
151
151
 
152
152
  self._init_search_runner()
153
-
153
+
154
154
  m.qps, m.conc_num_list, m.conc_qps_list, m.conc_latency_p99_list = self._conc_search()
155
155
  m.recall, m.serial_latency_p99 = self._serial_search()
156
156
  '''
@@ -176,6 +176,9 @@ class CaseRunner(BaseModel):
176
176
  or TaskStage.SEARCH_CONCURRENT in self.config.stages
177
177
  ):
178
178
  self._init_search_runner()
179
+ if TaskStage.SEARCH_CONCURRENT in self.config.stages:
180
+ search_results = self._conc_search()
181
+ m.qps, m.conc_num_list, m.conc_qps_list, m.conc_latency_p99_list, m.conc_latency_avg_list = search_results
179
182
  if TaskStage.SEARCH_SERIAL in self.config.stages:
180
183
  search_results = self._serial_search()
181
184
  '''
@@ -183,10 +186,7 @@ class CaseRunner(BaseModel):
183
186
  m.serial_latencies = search_results.serial_latencies
184
187
  '''
185
188
  m.recall, m.ndcg, m.serial_latency_p99 = search_results
186
- if TaskStage.SEARCH_CONCURRENT in self.config.stages:
187
- search_results = self._conc_search()
188
- m.qps, m.conc_num_list, m.conc_qps_list, m.conc_latency_p99_list = search_results
189
-
189
+
190
190
  except Exception as e:
191
191
  log.warning(f"Failed to run performance case, reason = {e}")
192
192
  traceback.print_exc()
@@ -9,6 +9,7 @@ from ..backend.clients.weaviate_cloud.cli import Weaviate
9
9
  from ..backend.clients.zilliz_cloud.cli import ZillizAutoIndex
10
10
  from ..backend.clients.milvus.cli import MilvusAutoIndex
11
11
  from ..backend.clients.aws_opensearch.cli import AWSOpenSearch
12
+ from ..backend.clients.alloydb.cli import AlloyDBScaNN
12
13
 
13
14
  from .cli import cli
14
15
 
@@ -24,6 +25,7 @@ cli.add_command(MilvusAutoIndex)
24
25
  cli.add_command(AWSOpenSearch)
25
26
  cli.add_command(PgVectorScaleDiskAnn)
26
27
  cli.add_command(PgDiskAnn)
28
+ cli.add_command(AlloyDBScaNN)
27
29
 
28
30
 
29
31
  if __name__ == "__main__":