Skip to content

Commit a0dbecc

Browse files
committed
feat(benchmarks): add benchmark suite and configuration
- Implement benchmark suite for sqlite-vec-client. - Add CRUD operation benchmarks with performance metrics. - Create configuration loader for benchmark settings. - Include utility functions for generating test data. - Add CSV export functionality for benchmark results.
1 parent 60d5b37 commit a0dbecc

File tree

11 files changed

+464
-0
lines changed

11 files changed

+464
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,5 @@ cython_debug/
205205
marimo/_static/
206206
marimo/_lsp/
207207
__marimo__/
208+
209+
*.csv

benchmarks/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Benchmark suite for sqlite-vec-client."""
2+
3+
from .config_loader import load_config
4+
from .operations import (
5+
benchmark_add,
6+
benchmark_delete_many,
7+
benchmark_get_all,
8+
benchmark_get_many,
9+
benchmark_similarity_search,
10+
benchmark_update_many,
11+
)
12+
from .reporter import export_to_csv, print_results, print_summary
13+
from .runner import run_benchmark_suite
14+
from .utils import generate_embeddings, generate_metadata, generate_texts
15+
16+
__all__ = [
17+
"load_config",
18+
"run_benchmark_suite",
19+
"print_results",
20+
"print_summary",
21+
"export_to_csv",
22+
"benchmark_add",
23+
"benchmark_get_many",
24+
"benchmark_similarity_search",
25+
"benchmark_update_many",
26+
"benchmark_get_all",
27+
"benchmark_delete_many",
28+
"generate_embeddings",
29+
"generate_texts",
30+
"generate_metadata",
31+
]

benchmarks/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .benchmark_crud import main
2+
3+
if __name__ == "__main__":
4+
main()

benchmarks/benchmark_crud.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Benchmark script for sqlite-vec-client CRUD operations.
2+
3+
Measures performance of all operations with varying dataset sizes.
4+
"""
5+
6+
import argparse
7+
8+
from .config_loader import load_config
9+
from .reporter import export_to_csv, print_results, print_summary
10+
from .runner import run_benchmark_suite
11+
12+
13+
def main():
14+
"""Run benchmarks with different dataset sizes."""
15+
parser = argparse.ArgumentParser(description="Run sqlite-vec-client benchmarks")
16+
parser.add_argument("-c", "--config", type=str, help="Path to config YAML file")
17+
parser.add_argument(
18+
"-o", "--output", type=str, help="Output directory for CSV export"
19+
)
20+
args = parser.parse_args()
21+
22+
config = load_config(args.config)
23+
24+
print("SQLite-Vec-Client Performance Benchmark")
25+
print("=" * 80)
26+
print(f"Configuration: dim={config['dimension']}, distance={config['distance']}")
27+
print("=" * 80)
28+
29+
dataset_sizes = config["dataset_sizes"]
30+
table_format = config["table_format"]
31+
db_modes = config.get("db_modes", ["file"])
32+
all_results = {}
33+
34+
for db_mode in db_modes:
35+
print(f"\n{'=' * 80}")
36+
print(f"Testing with {db_mode.upper()} database")
37+
print("=" * 80)
38+
39+
mode_results = {}
40+
for size in dataset_sizes:
41+
print(f"\nRunning benchmark with {size:,} records...")
42+
results = run_benchmark_suite(size, config, db_mode)
43+
mode_results[size] = results
44+
print_results(results, table_format)
45+
46+
all_results[db_mode] = mode_results
47+
48+
print_summary(all_results, dataset_sizes, table_format)
49+
50+
if args.output:
51+
export_to_csv(all_results, dataset_sizes, args.output)
52+
53+
54+
if __name__ == "__main__":
55+
main()

benchmarks/config.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Benchmark Configuration for sqlite-vec-client
2+
3+
# Dataset sizes to test (number of records)
4+
dataset_sizes:
5+
- 100
6+
- 1000
7+
- 10000
8+
- 50000
9+
10+
# Embedding dimension
11+
dimension: 384
12+
13+
# Distance metric (cosine, l2, or inner_product)
14+
distance: cosine
15+
16+
# Database modes to test (file, memory, or both)
17+
db_modes:
18+
- file
19+
- memory
20+
21+
# Similarity search configuration
22+
similarity_search:
23+
# Number of iterations for each search benchmark
24+
iterations: 100
25+
# Top-k values to test
26+
top_k_values:
27+
- 10
28+
- 100
29+
30+
# Batch size for get_all operation
31+
batch_size: 1000
32+
33+
# Output format (grid, fancy_grid, simple, plain, or any tabulate format)
34+
table_format: grid

benchmarks/config_loader.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Configuration loader for benchmarks."""
2+
3+
import os
4+
from typing import Any, Optional
5+
6+
import yaml # type: ignore[import-untyped]
7+
8+
9+
def load_config(config_path: Optional[str] = None) -> dict[str, Any]:
10+
"""Load benchmark configuration from YAML file."""
11+
if config_path is None:
12+
config_path = os.path.join(os.path.dirname(__file__), "config.yaml")
13+
14+
with open(config_path) as f:
15+
result: dict[str, Any] = yaml.safe_load(f)
16+
return result

benchmarks/operations.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Benchmark operations for CRUD methods."""
2+
3+
import statistics
4+
import time
5+
6+
from sqlite_vec_client import SQLiteVecClient
7+
8+
from .utils import benchmark_operation
9+
10+
11+
def benchmark_add(
12+
client: SQLiteVecClient,
13+
texts: list[str],
14+
embeddings: list[list[float]],
15+
metadata: list[dict],
16+
) -> dict:
17+
"""Benchmark add operations."""
18+
elapsed, rowids = benchmark_operation(
19+
client.add, texts=texts, embeddings=embeddings, metadata=metadata
20+
)
21+
return {
22+
"operation": "add",
23+
"count": len(texts),
24+
"time": elapsed,
25+
"ops_per_sec": len(texts) / elapsed,
26+
}
27+
28+
29+
def benchmark_get_many(client: SQLiteVecClient, rowids: list[int]) -> dict:
30+
"""Benchmark get_many operations."""
31+
elapsed, _ = benchmark_operation(client.get_many, rowids)
32+
return {
33+
"operation": "get_many",
34+
"count": len(rowids),
35+
"time": elapsed,
36+
"ops_per_sec": len(rowids) / elapsed,
37+
}
38+
39+
40+
def benchmark_similarity_search(
41+
client: SQLiteVecClient, embedding: list[float], top_k: int, iterations: int
42+
) -> dict:
43+
"""Benchmark similarity search operations."""
44+
times = []
45+
for _ in range(iterations):
46+
elapsed, _ = benchmark_operation(
47+
client.similarity_search, embedding=embedding, top_k=top_k
48+
)
49+
times.append(elapsed)
50+
51+
avg_time = statistics.mean(times)
52+
return {
53+
"operation": "similarity_search",
54+
"top_k": top_k,
55+
"iterations": iterations,
56+
"avg_time": avg_time,
57+
"min_time": min(times),
58+
"max_time": max(times),
59+
"searches_per_sec": 1 / avg_time,
60+
}
61+
62+
63+
def benchmark_update_many(
64+
client: SQLiteVecClient, rowids: list[int], texts: list[str]
65+
) -> dict:
66+
"""Benchmark update_many operations."""
67+
updates = [(rid, text, None, None) for rid, text in zip(rowids, texts)]
68+
elapsed, count = benchmark_operation(client.update_many, updates)
69+
return {
70+
"operation": "update_many",
71+
"count": count,
72+
"time": elapsed,
73+
"ops_per_sec": count / elapsed,
74+
}
75+
76+
77+
def benchmark_delete_many(client: SQLiteVecClient, rowids: list[int]) -> dict:
78+
"""Benchmark delete_many operations."""
79+
elapsed, count = benchmark_operation(client.delete_many, rowids)
80+
return {
81+
"operation": "delete_many",
82+
"count": count,
83+
"time": elapsed,
84+
"ops_per_sec": count / elapsed,
85+
}
86+
87+
88+
def benchmark_get_all(
89+
client: SQLiteVecClient, expected_count: int, batch_size: int
90+
) -> dict:
91+
"""Benchmark get_all operations."""
92+
93+
start = time.perf_counter()
94+
count = sum(1 for _ in client.get_all(batch_size=batch_size))
95+
elapsed = time.perf_counter() - start
96+
return {
97+
"operation": "get_all",
98+
"count": count,
99+
"time": elapsed,
100+
"ops_per_sec": count / elapsed,
101+
}

benchmarks/reporter.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""Benchmark results reporting."""
2+
3+
import csv
4+
import os
5+
from datetime import datetime
6+
7+
from tabulate import tabulate # type: ignore[import-untyped]
8+
9+
10+
def print_results(results: list[dict], table_format: str):
11+
"""Print benchmark results in a formatted table."""
12+
table_data = []
13+
for result in results:
14+
op = result["operation"]
15+
if "top_k" in result:
16+
op = f"{op} (k={result['top_k']})"
17+
18+
count = result.get("count", result.get("iterations", "-"))
19+
time_val = result.get("time", result.get("avg_time", 0))
20+
ops_per_sec = result.get("ops_per_sec", result.get("searches_per_sec", 0))
21+
22+
table_data.append([op, count, f"{time_val:.4f}", f"{ops_per_sec:.2f}"])
23+
24+
print(
25+
"\n"
26+
+ tabulate(
27+
table_data,
28+
headers=["Operation", "Count", "Time (s)", "Ops/sec"],
29+
tablefmt=table_format,
30+
)
31+
)
32+
33+
34+
def print_summary(
35+
all_results: dict[str, dict[int, list[dict]]],
36+
dataset_sizes: list[int],
37+
table_format: str,
38+
):
39+
"""Print summary table of all benchmark results."""
40+
for db_mode, mode_results in all_results.items():
41+
print("\n" + "=" * 80)
42+
print(f"SUMMARY - Operations per Second by Dataset Size ({db_mode.upper()} DB)")
43+
print("=" * 80)
44+
45+
operations = [
46+
"add",
47+
"get_many",
48+
"similarity_search",
49+
"update_many",
50+
"get_all",
51+
"delete_many",
52+
]
53+
summary_data = []
54+
for op in operations:
55+
row = [op]
56+
for size in dataset_sizes:
57+
matching = [r for r in mode_results[size] if r["operation"] == op]
58+
if matching:
59+
ops_per_sec = matching[0].get(
60+
"ops_per_sec", matching[0].get("searches_per_sec", 0)
61+
)
62+
row.append(f"{ops_per_sec:,.0f}")
63+
else:
64+
row.append("N/A")
65+
summary_data.append(row)
66+
67+
headers = ["Operation"] + [f"{s:,}" for s in dataset_sizes]
68+
print(tabulate(summary_data, headers=headers, tablefmt=table_format))
69+
print("=" * 80)
70+
71+
72+
def export_to_csv(
73+
all_results: dict[str, dict[int, list[dict]]],
74+
dataset_sizes: list[int],
75+
output_dir: str,
76+
):
77+
"""Export benchmark results to CSV files."""
78+
os.makedirs(output_dir, exist_ok=True)
79+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
80+
81+
for db_mode, mode_results in all_results.items():
82+
for size in dataset_sizes:
83+
filename = os.path.join(
84+
output_dir, f"benchmark_{db_mode}_{size}_{timestamp}.csv"
85+
)
86+
87+
with open(filename, "w", newline="") as f:
88+
writer = csv.writer(f)
89+
writer.writerow(["Operation", "ops_per_sec", "time_sec"])
90+
91+
operations = [
92+
"add",
93+
"get_many",
94+
"similarity_search",
95+
"update_many",
96+
"get_all",
97+
"delete_many",
98+
]
99+
for op in operations:
100+
matching = [r for r in mode_results[size] if r["operation"] == op]
101+
if matching:
102+
ops_per_sec = matching[0].get(
103+
"ops_per_sec", matching[0].get("searches_per_sec", 0)
104+
)
105+
time_val = matching[0].get(
106+
"time", matching[0].get("avg_time", 0)
107+
)
108+
writer.writerow([op, f"{ops_per_sec:.2f}", f"{time_val:.4f}"])
109+
else:
110+
writer.writerow([op, "N/A", "N/A"])
111+
112+
print(f"Exported {db_mode} ({size} records) to: {filename}")

0 commit comments

Comments
 (0)