Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions cmd/vector-db-bench/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# vector-db-bench

`vector-db-bench` is a native Go benchmark driver for xvec. It follows the
VectorDBBench Cohere performance workload used by the Alibaba zvec benchmark
guide:
`vector-db-bench` is a native Go benchmark driver for comparing
[xvec](https://github.com/gorse-io/xvec) and
[zvec-go](https://github.com/zvec-ai/zvec-go). It follows the VectorDBBench
Cohere performance workload used by the Alibaba zvec benchmark guide:

- Cohere 1M and 10M Parquet datasets;
- HNSW loading and optimization;
Expand All @@ -18,7 +19,20 @@ substantially more.
## Build

```bash
go build -o vector-db-bench ./cmd/vector-db-bench
CGO_ENABLED=0 go build -o vector-db-bench ./cmd/vector-db-bench
```

The pure-Go build keeps xvec and zvec-go in one binary without linking zvec at
build time. Running the `zvec` backend requires the zvec C API shared library.
Download the archive for your platform from the
[zvec-go v0.6.0 release](https://github.com/zvec-ai/zvec-go/releases/tag/v0.6.0),
extract it, and point `ZVEC_LIBRARY_PATH` to the extracted library or its
directory. The xvec backend does not require this library.

For example, on Linux x86-64:

```bash
export ZVEC_LIBRARY_PATH=/path/to/linux_amd64/libzvec_c_api.so
```

## Cohere 1M
Expand All @@ -28,7 +42,7 @@ first invocation downloads the data, recreates the collection, loads it, and
runs both search phases.

```bash
./vector-db-bench \
./vector-db-bench xvec \
--path ./Performance768D1M \
--case-type Performance768D1M \
--num-concurrency 12,14,16,18,20 \
Expand All @@ -37,10 +51,23 @@ runs both search phases.
--output result-cohere-1m.json
```

Run the same workload against zvec-go by changing only the backend and output
path:

```bash
./vector-db-bench zvec \
--path ./Performance768D1M-zvec \
--case-type Performance768D1M \
--num-concurrency 12,14,16,18,20 \
--m 15 \
--ef-search 180 \
--output result-zvec-cohere-1m.json
```

To rerun only the search phases against that collection:

```bash
./vector-db-bench \
./vector-db-bench xvec \
--path ./Performance768D1M \
--case-type Performance768D1M \
--num-concurrency 12,14,16,18,20 \
Expand All @@ -56,7 +83,7 @@ To rerun only the search phases against that collection:
This mirrors the published INT8/refiner configuration:

```bash
./vector-db-bench \
./vector-db-bench xvec \
--path ./Performance768D10M \
--case-type Performance768D10M \
--num-concurrency 12,14,16,18,20 \
Expand Down Expand Up @@ -88,7 +115,7 @@ The built-in cases use the VectorDBBench schema:
A local custom dataset can be exercised without downloads:

```bash
./vector-db-bench \
./vector-db-bench xvec \
--path ./custom-collection \
--case-type Custom \
--dataset-dir ./dataset \
Expand Down
58 changes: 58 additions & 0 deletions cmd/vector-db-bench/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2026-present the xvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"context"
"fmt"
"io"

"github.com/gorse-io/xvec"
)

func initializeBenchmarkBackend(backend string) (func(), error) {
if backend == backendZvec {
return initializeZvecBackend()
}
return func() {}, nil
}

func loadBenchmarkDataset(ctx context.Context, config benchConfig, log io.Writer) (loadMetrics, error) {
switch config.Backend {
case backendXvec:
return loadXvecDataset(ctx, config, log)
case backendZvec:
return loadZvecDataset(ctx, config, log)
default:
return loadMetrics{}, fmt.Errorf("unsupported backend %q", config.Backend)
}
}

func openBenchmarkQueryEngine(ctx context.Context, config benchConfig) (benchmarkQueryEngine, io.Closer, error) {
switch config.Backend {
case backendXvec:
collection, err := xvec.Open(ctx, config.Path, xvec.CollectionOptions{
ReadOnly: true, EnableMmap: config.EnableMmap, MaxBufferSize: uint32(config.MaxBufferSize),
})
if err != nil {
return nil, nil, fmt.Errorf("open xvec benchmark collection for search: %w", err)
}
return newXvecQueryEngine(collection, config), collection, nil
case backendZvec:
return openZvecQueryEngine(config)
default:
return nil, nil, fmt.Errorf("unsupported backend %q", config.Backend)
}
}
53 changes: 53 additions & 0 deletions cmd/vector-db-bench/backend_stub_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2026-present the xvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build cgo && !purego

package main

import (
"bytes"
"context"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

func TestBenchmarkBackendDispatchAndZvecStub(t *testing.T) {
shutdown, err := initializeBenchmarkBackend(backendXvec)
require.NoError(t, err)
require.NotNil(t, shutdown)
shutdown()

_, err = initializeBenchmarkBackend(backendZvec)
require.ErrorIs(t, err, errZvecBackendUnavailable)

config := benchConfig{Backend: backendZvec}
_, err = loadBenchmarkDataset(context.Background(), config, &bytes.Buffer{})
require.ErrorIs(t, err, errZvecBackendUnavailable)
_, _, err = openBenchmarkQueryEngine(context.Background(), config)
require.ErrorIs(t, err, errZvecBackendUnavailable)

_, err = loadBenchmarkDataset(context.Background(), benchConfig{Backend: "unknown"}, &bytes.Buffer{})
require.ErrorContains(t, err, "unsupported backend")
_, _, err = openBenchmarkQueryEngine(context.Background(), benchConfig{Backend: "unknown"})
require.ErrorContains(t, err, "unsupported backend")

_, _, err = openBenchmarkQueryEngine(context.Background(), benchConfig{
Backend: backendXvec,
Path: filepath.Join(t.TempDir(), "missing"),
})
require.ErrorContains(t, err, "open xvec benchmark collection")
}
36 changes: 21 additions & 15 deletions cmd/vector-db-bench/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@ import (

func runBenchmark(ctx context.Context, config benchConfig, log io.Writer) (benchmarkReport, error) {
report := newBenchmarkReport(config)
shutdown, err := initializeBenchmarkBackend(config.Backend)
if err != nil {
return report, err
}
defer shutdown()
needsSearch := !config.SkipSerialSearch || !config.SkipConcurrentSearch
if !config.SkipLoad || needsSearch {
if err := prepareDataset(ctx, config, !config.SkipLoad, log); err != nil {
return report, err
}
}
if !config.SkipLoad {
metrics, err := loadDataset(ctx, config, log)
metrics, err := loadBenchmarkDataset(ctx, config, log)
if err != nil {
return report, err
}
Expand All @@ -53,14 +58,11 @@ func runBenchmark(ctx context.Context, config benchConfig, log io.Writer) (bench
if err != nil {
return report, err
}
collection, err := xvec.Open(ctx, config.Path, xvec.CollectionOptions{
ReadOnly: true, EnableMmap: config.EnableMmap, MaxBufferSize: uint32(config.MaxBufferSize),
})
engine, closer, err := openBenchmarkQueryEngine(ctx, config)
if err != nil {
return report, fmt.Errorf("open benchmark collection for search: %w", err)
return report, err
}
defer func() { _ = collection.Close() }()
engine := newQueryEngine(collection, config)
defer func() { _ = closer.Close() }()
if err := warmupSearch(ctx, engine, data, config.WarmupQueries); err != nil {
return report, err
}
Expand Down Expand Up @@ -94,7 +96,7 @@ func runBenchmark(ctx context.Context, config benchConfig, log io.Writer) (bench
return report, nil
}

func loadDataset(ctx context.Context, config benchConfig, log io.Writer) (loadMetrics, error) {
func loadXvecDataset(ctx context.Context, config benchConfig, log io.Writer) (loadMetrics, error) {
collection, err := writableBenchmarkCollection(ctx, config)
if err != nil {
return loadMetrics{}, err
Expand Down Expand Up @@ -243,20 +245,24 @@ func parseQuantization(value string) (xvec.QuantizeType, error) {
}
}

type queryEngine struct {
type benchmarkQueryEngine interface {
search(context.Context, []float32) ([]int64, error)
}

type xvecQueryEngine struct {
collection *xvec.Collection
params xvec.HNSWQueryParams
k int
}

func newQueryEngine(collection *xvec.Collection, config benchConfig) queryEngine {
func newXvecQueryEngine(collection *xvec.Collection, config benchConfig) xvecQueryEngine {
params := xvec.NewHNSWQueryParams()
params.EF = config.EFSearch
params.UseRefiner = config.UseRefiner
return queryEngine{collection: collection, params: params, k: config.K}
return xvecQueryEngine{collection: collection, params: params, k: config.K}
}

func (e queryEngine) search(ctx context.Context, vector []float32) ([]int64, error) {
func (e xvecQueryEngine) search(ctx context.Context, vector []float32) ([]int64, error) {
results, err := e.collection.Query(ctx, xvec.VectorQuery{
Field: "dense", DenseVector: xvec.VectorFP32(vector), TopK: e.k, Params: e.params,
Projection: xvec.Projection{OutputFields: []string{}},
Expand All @@ -274,7 +280,7 @@ func (e queryEngine) search(ctx context.Context, vector []float32) ([]int64, err
return ids, nil
}

func warmupSearch(ctx context.Context, engine queryEngine, data queryData, count int) error {
func warmupSearch(ctx context.Context, engine benchmarkQueryEngine, data queryData, count int) error {
if count > len(data.Vectors) {
count = len(data.Vectors)
}
Expand All @@ -286,7 +292,7 @@ func warmupSearch(ctx context.Context, engine queryEngine, data queryData, count
return nil
}

func runSerialSearch(ctx context.Context, engine queryEngine, data queryData, k int) (searchMetrics, error) {
func runSerialSearch(ctx context.Context, engine benchmarkQueryEngine, data queryData, k int) (searchMetrics, error) {
latencies := make([]float64, len(data.Vectors))
var recall float64
started := time.Now()
Expand All @@ -312,7 +318,7 @@ type concurrentWorkerResult struct {

func runConcurrentSearch(
ctx context.Context,
engine queryEngine,
engine benchmarkQueryEngine,
data queryData,
concurrency int,
duration time.Duration,
Expand Down
7 changes: 7 additions & 0 deletions cmd/vector-db-bench/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ func TestRecallPercentileAndSearchSummary(t *testing.T) {
}

func TestVectorDBBenchEndToEndCustomDataset(t *testing.T) {
testVectorDBBenchEndToEndCustomDataset(t, backendXvec)
}

func testVectorDBBenchEndToEndCustomDataset(t *testing.T, backend string) {
t.Helper()
directory := t.TempDir()
datasetDir := filepath.Join(directory, "dataset")
require.NoError(t, mkdir(datasetDir))
Expand All @@ -62,6 +67,7 @@ func TestVectorDBBenchEndToEndCustomDataset(t *testing.T) {

var stdout, stderr bytes.Buffer
err := runCLI(context.Background(), []string{
backend,
"--path", filepath.Join(directory, "collection"),
"--case-type", caseCustom,
"--dataset-dir", datasetDir,
Expand All @@ -82,6 +88,7 @@ func TestVectorDBBenchEndToEndCustomDataset(t *testing.T) {
require.NoError(t, err, stderr.String())
var report benchmarkReport
require.NoError(t, json.Unmarshal(stdout.Bytes(), &report), fmt.Sprintf("stdout: %s", stdout.String()))
require.Equal(t, backend, report.Config.Backend)
require.NotNil(t, report.Load)
require.Equal(t, int64(32), report.Load.Rows)
require.NotNil(t, report.Serial)
Expand Down
17 changes: 14 additions & 3 deletions cmd/vector-db-bench/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import (
)

const (
backendXvec = "xvec"
backendZvec = "zvec"

casePerformance768D1M = "Performance768D1M"
casePerformance768D10M = "Performance768D10M"
caseCustom = "Custom"
Expand All @@ -43,6 +46,7 @@ type benchmarkCase struct {
}

type benchConfig struct {
Backend string
Path string
CaseType string
DatasetDir string
Expand Down Expand Up @@ -84,9 +88,16 @@ type benchConfig struct {

func parseConfig(args []string, stderr io.Writer) (benchConfig, error) {
var config benchConfig
if len(args) == 0 || strings.HasPrefix(args[0], "-") {
return benchConfig{}, errors.New("backend is required: xvec or zvec")
}
config.Backend = strings.ToLower(args[0])
if config.Backend != backendXvec && config.Backend != backendZvec {
return benchConfig{}, fmt.Errorf("unsupported backend %q: use xvec or zvec", args[0])
}
flags := flag.NewFlagSet("vector-db-bench", flag.ContinueOnError)
flags.SetOutput(stderr)
flags.StringVar(&config.Path, "path", "", "xvec collection path (required)")
flags.StringVar(&config.Path, "path", "", "collection path (required)")
flags.StringVar(&config.CaseType, "case-type", casePerformance768D1M, "benchmark case: Performance768D1M, Performance768D10M, or Custom")
flags.StringVar(&config.DatasetDir, "dataset-dir", "", "local VectorDBBench dataset directory")
flags.StringVar(&config.DatasetBaseURL, "dataset-base-url", "https://assets.zilliz.com/benchmark", "VectorDBBench dataset base URL")
Expand Down Expand Up @@ -117,11 +128,11 @@ func parseConfig(args []string, stderr io.Writer) (benchConfig, error) {
flags.BoolVar(&config.SkipConcurrentSearch, "skip-search-concurrent", false, "skip sustained concurrent search")
flags.BoolVar(&config.DryRun, "dry-run", false, "validate and print configuration without downloading or running")
flags.StringVar(&config.Output, "output", "", "write result JSON to this file; empty writes JSON to stdout")
flags.StringVar(&config.DBLabel, "db-label", "xvec-go", "label stored in the result")
flags.StringVar(&config.DBLabel, "db-label", config.Backend+"-go", "label stored in the result")
flags.StringVar(&config.Note, "note", "", "non-sensitive run context stored in the result")
operationTimeout := flags.String("operation-timeout", "0", "whole-run timeout; zero disables it")
flags.Int64Var(&config.Seed, "seed", 0, "deterministic concurrent-query seed")
if err := flags.Parse(args); err != nil {
if err := flags.Parse(args[1:]); err != nil {
return benchConfig{}, err
}
if flags.NArg() != 0 {
Expand Down
Loading
Loading