Skip to content

Commit 27c5f54

Browse files
committed
feat: add VectorChordGraph support and configuration 🎉✨
- Introduced VectorChordGraph command to CLI for enhanced functionality. - Added quantization and reranking options to VectorChord configurations.
1 parent bcf95dc commit 27c5f54

5 files changed

Lines changed: 227 additions & 28 deletions

File tree

‎vectordb_bench/backend/clients/api.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class IndexType(StrEnum):
4343
GPU_CAGRA = "GPU_CAGRA"
4444
SCANN = "scann"
4545
VCHORDRQ = "vchordrq"
46+
VCHORDG = "vchordg"
4647
SCANN_MILVUS = "SCANN_MILVUS"
4748
Hologres_HGraph = "HGraph"
4849
Hologres_Graph = "Graph"

‎vectordb_bench/backend/clients/vectorchord/cli.py‎

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ class VectorChordTypedDict(CommonTypedDict):
4343
),
4444
]
4545
db_name: Annotated[str, click.option("--db-name", type=str, help="Db name", required=True)]
46+
max_parallel_workers: Annotated[
47+
int | None,
48+
click.option(
49+
"--max-parallel-workers",
50+
type=int,
51+
help="Sets the maximum number of parallel workers for index creation",
52+
required=False,
53+
),
54+
]
55+
quantization_type: Annotated[
56+
str | None,
57+
click.option(
58+
"--quantization-type",
59+
type=click.Choice(["vector", "halfvec", "rabitq8", "rabitq4"]),
60+
help="Quantization type for vectors",
61+
default="vector",
62+
show_default=True,
63+
),
64+
]
4665

4766

4867
class VectorChordRQTypedDict(VectorChordTypedDict):
@@ -84,6 +103,16 @@ class VectorChordRQTypedDict(VectorChordTypedDict):
84103
show_default=True,
85104
),
86105
]
106+
rerank_in_table: Annotated[
107+
bool,
108+
click.option(
109+
"--rerank-in-table/--no-rerank-in-table",
110+
type=bool,
111+
help="Read vectors from table instead of storing in index (saves storage, degrades query performance)",
112+
default=False,
113+
show_default=True,
114+
),
115+
]
87116
spherical_centroids: Annotated[
88117
bool,
89118
click.option(
@@ -118,14 +147,6 @@ class VectorChordRQTypedDict(VectorChordTypedDict):
118147
help="Max tuples to scan before stopping (-1 for unlimited)",
119148
),
120149
]
121-
max_parallel_workers: Annotated[
122-
int | None,
123-
click.option(
124-
"--max-parallel-workers",
125-
type=int,
126-
help="Sets the maximum number of parallel workers for index creation",
127-
),
128-
]
129150

130151

131152
@cli.command()
@@ -146,10 +167,12 @@ def VectorChordRQ(
146167
db_name=parameters["db_name"],
147168
),
148169
db_case_config=VectorChordRQConfig(
170+
quantization_type=parameters["quantization_type"],
149171
lists=parameters["lists"],
150172
probes=parameters["probes"],
151173
epsilon=parameters["epsilon"],
152174
residual_quantization=parameters["residual_quantization"],
175+
rerank_in_table=parameters["rerank_in_table"],
153176
spherical_centroids=parameters["spherical_centroids"],
154177
build_threads=parameters["build_threads"],
155178
degree_of_parallelism=parameters["degree_of_parallelism"],
@@ -158,3 +181,78 @@ def VectorChordRQ(
158181
),
159182
**parameters,
160183
)
184+
185+
186+
class VectorChordGraphTypedDict(VectorChordTypedDict):
187+
m: Annotated[
188+
int | None,
189+
click.option(
190+
"--m",
191+
type=int,
192+
help="Max neighbors per vertex (default: 32)",
193+
),
194+
]
195+
ef_construction: Annotated[
196+
int | None,
197+
click.option(
198+
"--ef-construction",
199+
type=int,
200+
help="Dynamic list size during insertion (default: 64)",
201+
),
202+
]
203+
bits: Annotated[
204+
int | None,
205+
click.option(
206+
"--bits",
207+
type=int,
208+
help="RaBitQ quantization ratio (1 or 2, default: 2)",
209+
),
210+
]
211+
ef_search: Annotated[
212+
int | None,
213+
click.option(
214+
"--ef-search",
215+
type=int,
216+
help="Dynamic list size for search (default: 64)",
217+
default=64,
218+
show_default=True,
219+
),
220+
]
221+
beam_search: Annotated[
222+
int | None,
223+
click.option(
224+
"--beam-search",
225+
type=int,
226+
help="Batch vertex access width during search (default: 1)",
227+
),
228+
]
229+
230+
231+
@cli.command()
232+
@click_parameter_decorators_from_typed_dict(VectorChordGraphTypedDict)
233+
def VectorChordGraph(
234+
**parameters: Unpack[VectorChordGraphTypedDict],
235+
):
236+
from .config import VectorChordConfig, VectorChordGraphConfig
237+
238+
run(
239+
db=DB.VectorChord,
240+
db_config=VectorChordConfig(
241+
db_label=parameters["db_label"],
242+
user_name=SecretStr(parameters["user_name"]),
243+
password=SecretStr(parameters["password"]),
244+
host=parameters["host"],
245+
port=parameters["port"],
246+
db_name=parameters["db_name"],
247+
),
248+
db_case_config=VectorChordGraphConfig(
249+
quantization_type=parameters["quantization_type"],
250+
m=parameters["m"],
251+
ef_construction=parameters["ef_construction"],
252+
bits=parameters["bits"],
253+
ef_search=parameters["ef_search"],
254+
beam_search=parameters["beam_search"],
255+
max_parallel_workers=parameters["max_parallel_workers"],
256+
),
257+
**parameters,
258+
)

‎vectordb_bench/backend/clients/vectorchord/config.py‎

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,39 @@ def to_dict(self) -> VectorChordConfigDict:
3636
}
3737

3838

39+
_METRIC_OPS = {
40+
"vector": {
41+
MetricType.L2: "vector_l2_ops",
42+
MetricType.IP: "vector_ip_ops",
43+
MetricType.COSINE: "vector_cosine_ops",
44+
},
45+
"halfvec": {
46+
MetricType.L2: "halfvec_l2_ops",
47+
MetricType.IP: "halfvec_ip_ops",
48+
MetricType.COSINE: "halfvec_cosine_ops",
49+
},
50+
"rabitq8": {
51+
MetricType.L2: "rabitq8_l2_ops",
52+
MetricType.IP: "rabitq8_ip_ops",
53+
MetricType.COSINE: "rabitq8_cosine_ops",
54+
},
55+
"rabitq4": {
56+
MetricType.L2: "rabitq4_l2_ops",
57+
MetricType.IP: "rabitq4_ip_ops",
58+
MetricType.COSINE: "rabitq4_cosine_ops",
59+
},
60+
}
61+
62+
3963
class VectorChordIndexConfig(BaseModel, DBCaseConfig):
4064
metric_type: MetricType | None = None
4165
create_index_before_load: bool = False
4266
create_index_after_load: bool = True
67+
quantization_type: str = "vector" # vector, halfvec, rabitq8, rabitq4
4368

4469
def parse_metric(self) -> str:
45-
if self.metric_type == MetricType.L2:
46-
return "vector_l2_ops"
47-
if self.metric_type == MetricType.IP:
48-
return "vector_ip_ops"
49-
return "vector_cosine_ops"
70+
ops = _METRIC_OPS.get(self.quantization_type, _METRIC_OPS["vector"])
71+
return ops.get(self.metric_type, ops[MetricType.COSINE])
5072

5173
def parse_metric_fun_op(self) -> LiteralString:
5274
if self.metric_type == MetricType.L2:
@@ -69,6 +91,7 @@ class VectorChordRQConfig(VectorChordIndexConfig):
6991
index: IndexType = IndexType.VCHORDRQ
7092
# Build parameters (top-level options)
7193
residual_quantization: bool = False
94+
rerank_in_table: bool = False
7295
degree_of_parallelism: int | None = None # default 32, range [1, 256]
7396
# Build parameters ([build.internal] section)
7497
lists: int | None = None
@@ -83,6 +106,8 @@ class VectorChordRQConfig(VectorChordIndexConfig):
83106

84107
def index_param(self) -> dict:
85108
options_parts = []
109+
if self.rerank_in_table:
110+
options_parts.append("rerank_in_table = true")
86111
if self.residual_quantization:
87112
options_parts.append("residual_quantization = true")
88113
if self.degree_of_parallelism is not None:
@@ -98,6 +123,7 @@ def index_param(self) -> dict:
98123
return {
99124
"metric": self.parse_metric(),
100125
"index_type": self.index.value,
126+
"quantization_type": self.quantization_type,
101127
"options": "\n".join(options_parts),
102128
"max_parallel_workers": self.max_parallel_workers,
103129
}
@@ -118,6 +144,50 @@ def session_param(self) -> dict:
118144
return params
119145

120146

147+
class VectorChordGraphConfig(VectorChordIndexConfig):
148+
index: IndexType = IndexType.VCHORDG
149+
# Build parameters
150+
m: int | None = None # default 32, max neighbors per vertex
151+
ef_construction: int | None = None # default 64
152+
bits: int | None = None # default 2, quantization ratio (1 or 2)
153+
# PostgreSQL tuning parameter
154+
max_parallel_workers: int | None = None
155+
# Search parameters (GUCs)
156+
ef_search: int | None = 64 # range [1, 65535]
157+
beam_search: int | None = None # default 1
158+
159+
def index_param(self) -> dict:
160+
options_parts = []
161+
if self.m is not None:
162+
options_parts.append(f"m = {self.m}")
163+
if self.ef_construction is not None:
164+
options_parts.append(f"ef_construction = {self.ef_construction}")
165+
if self.bits is not None:
166+
options_parts.append(f"bits = {self.bits}")
167+
168+
return {
169+
"metric": self.parse_metric(),
170+
"index_type": self.index.value,
171+
"quantization_type": self.quantization_type,
172+
"options": "\n".join(options_parts),
173+
"max_parallel_workers": self.max_parallel_workers,
174+
}
175+
176+
def search_param(self) -> dict:
177+
return {
178+
"metric_fun_op": self.parse_metric_fun_op(),
179+
}
180+
181+
def session_param(self) -> dict:
182+
params = {}
183+
if self.ef_search is not None:
184+
params["vchordg.ef_search"] = str(self.ef_search)
185+
if self.beam_search is not None:
186+
params["vchordg.beam_search"] = str(self.beam_search)
187+
return params
188+
189+
121190
_vectorchord_case_config = {
122191
IndexType.VCHORDRQ: VectorChordRQConfig,
192+
IndexType.VCHORDG: VectorChordGraphConfig,
123193
}

‎vectordb_bench/backend/clients/vectorchord/vectorchord.py‎

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def __init__(
4444
self._primary_field = "id"
4545
self._vector_field = "embedding"
4646

47+
index_param = self.case_config.index_param()
48+
self._quantization_type = index_param["quantization_type"]
49+
self._index_method = index_param["index_type"]
50+
4751
self.conn, self.cursor = self._create_connection(**self.db_config)
4852

4953
log.info(f"{self.name} config values: {self.db_config}\n{self.case_config}")
@@ -103,13 +107,16 @@ def init(self) -> Generator[None, None, None]:
103107
self.cursor.execute(command)
104108
self.conn.commit()
105109

110+
# Search query cast type: rabitq8/rabitq4 queries still accept ::vector input
111+
cast_type = "vector"
112+
106113
self._filtered_search = sql.Composed(
107114
[
108115
sql.SQL("SELECT id FROM public.{} WHERE id >= %s ORDER BY embedding ").format(
109116
sql.Identifier(self.table_name),
110117
),
111118
sql.SQL(self.case_config.search_param()["metric_fun_op"]),
112-
sql.SQL(" %s::vector LIMIT %s::int"),
119+
sql.SQL(f" %s::{cast_type} LIMIT %s::int"),
113120
],
114121
)
115122

@@ -119,7 +126,7 @@ def init(self) -> Generator[None, None, None]:
119126
sql.Identifier(self.table_name),
120127
),
121128
sql.SQL(self.case_config.search_param()["metric_fun_op"]),
122-
sql.SQL(" %s::vector LIMIT %s::int"),
129+
sql.SQL(f" %s::{cast_type} LIMIT %s::int"),
123130
],
124131
)
125132

@@ -204,18 +211,19 @@ def _create_index(self):
204211
index_create_sql = sql.SQL(
205212
"""
206213
CREATE INDEX IF NOT EXISTS {index_name} ON public.{table_name}
207-
USING vchordrq (embedding {embedding_metric})
214+
USING {index_method} (embedding {embedding_metric})
208215
""",
209216
).format(
210217
index_name=sql.Identifier(self._index_name),
211218
table_name=sql.Identifier(self.table_name),
219+
index_method=sql.SQL(self._index_method),
212220
embedding_metric=sql.Identifier(index_param["metric"]),
213221
)
214222

215223
options_str = index_param.get("options", "")
216224
if options_str:
217225
with_clause = sql.SQL(
218-
"WITH (options = $vchordrq$\n{options}\n$vchordrq$);",
226+
"WITH (options = $vchord$\n{options}\n$vchord$);",
219227
).format(options=sql.SQL(options_str))
220228
else:
221229
with_clause = sql.SQL(";")
@@ -232,10 +240,20 @@ def _create_table(self, dim: int):
232240
try:
233241
log.info(f"{self.name} client create table : {self.table_name}")
234242

243+
col_type = self._quantization_type
244+
if col_type in ("rabitq8", "rabitq4"):
245+
# rabitq types need vector column + quantization during insert
246+
col_type = "vector"
247+
235248
self.cursor.execute(
236249
sql.SQL(
237-
"CREATE TABLE IF NOT EXISTS public.{table_name} (id BIGINT PRIMARY KEY, embedding vector({dim}));",
238-
).format(table_name=sql.Identifier(self.table_name), dim=dim),
250+
"CREATE TABLE IF NOT EXISTS public.{table_name} "
251+
"(id BIGINT PRIMARY KEY, embedding {col_type}({dim}));",
252+
).format(
253+
table_name=sql.Identifier(self.table_name),
254+
col_type=sql.SQL(col_type),
255+
dim=dim,
256+
),
239257
)
240258
self.conn.commit()
241259
except Exception as e:
@@ -255,14 +273,25 @@ def insert_embeddings(
255273
metadata_arr = np.array(metadata)
256274
embeddings_arr = np.array(embeddings)
257275

258-
with self.cursor.copy(
259-
sql.SQL("COPY public.{table_name} FROM STDIN (FORMAT BINARY)").format(
260-
table_name=sql.Identifier(self.table_name),
261-
),
262-
) as copy:
263-
copy.set_types(["bigint", "vector"])
264-
for i, row in enumerate(metadata_arr):
265-
copy.write_row((row, embeddings_arr[i]))
276+
if self._quantization_type == "halfvec":
277+
with self.cursor.copy(
278+
sql.SQL("COPY public.{table_name} FROM STDIN (FORMAT BINARY)").format(
279+
table_name=sql.Identifier(self.table_name),
280+
),
281+
) as copy:
282+
copy.set_types(["bigint", "halfvec"])
283+
for i, row in enumerate(metadata_arr):
284+
copy.write_row((row, np.float16(embeddings_arr[i])))
285+
else:
286+
# vector, rabitq8, rabitq4 all store as vector column
287+
with self.cursor.copy(
288+
sql.SQL("COPY public.{table_name} FROM STDIN (FORMAT BINARY)").format(
289+
table_name=sql.Identifier(self.table_name),
290+
),
291+
) as copy:
292+
copy.set_types(["bigint", "vector"])
293+
for i, row in enumerate(metadata_arr):
294+
copy.write_row((row, embeddings_arr[i]))
266295
self.conn.commit()
267296

268297
return len(metadata), None

0 commit comments

Comments
 (0)