Skip to content

feat: parquet loading, pgvector support, multi-table datasets, and cohere-10m vector dataset - #67

Open
rebasedming wants to merge 24 commits into
mainfrom
feat/vector-parquet
Open

feat: parquet loading, pgvector support, multi-table datasets, and cohere-10m vector dataset#67
rebasedming wants to merge 24 commits into
mainfrom
feat/vector-parquet

Conversation

@rebasedming

@rebasedming rebasedming commented Aug 31, 2026

Copy link
Copy Markdown

Description

Adds everything needed to run vector benchmarks against the Cohere Wikipedia 10M dataset (the same corpus used by paradedb/paradedb CI), comparing the pg_search vector index against pgvector HNSW — plus multi-table dataset loading, groundwork for normalized-dataset benchmarks (e.g. StackOverflow joins).

Loader / driver changes

  • Parquet input: loader load now accepts data.parquet, or a data/ directory of sharded parquet files (the S3 layout), alongside data.csv. Reading is streamed per row group; shards are chained with a column-consistency check.
  • Multi-table datasets: schema.yaml accepts a tables list (one entry per table with its own columns) as an alternative to the single top-level table/columns form. Each table resolves its own data source under data/ with the same preference order (<table>.parquet, <table>.csv, <table>/ shard directory); tables load sequentially and drop removes all of them. Validation rejects mixed forms, nested entries, and duplicates, and names the failing table in per-backend column-type errors. Single-table datasets are unchanged.
  • vector(n) schema type: CSV cells parse JSON arrays; parquet list<float> columns map directly to pgvector.Vector.
  • smallint schema type: supported on both CSV and parquet paths with int16 bounds checks. Conversion happens in the shared RowSource layer, so every backend receives a Go int16 (pgx encodes int2 over binary COPY; ClickHouse DDL should use Int16).
  • pgvector type registration: the shared postgres driver registers pgvector codecs in AfterConnect (best-effort), and a new TypeReloader hook resets the pool after pre scripts run — required because pre.sql does CREATE EXTENSION vector on connections that predate the extension.

New dataset: datasets/cohere-10m/

  • Schema/table matching paradedb/paradedb benchmarks/datasets/cohere (cohere_wiki, emb vector(1024)).
  • post.sql builds the real pg_search vector index (USING bm25 ... emb vector_cosine_ops) vs pgvector HNSW, with the recall knobs (paradedb.vector_cluster_max_probe, hnsw.ef_search) set via ALTER DATABASE and documented as the operating points to tune.
  • Docker compose (paradedb v0.25.6-pg18, pgvector pg18), k6 KNN script with staggered phases, and a script converting the official cohere_queries.parquet into a k6 query pool.
  • README covers pulling the sharded data from S3 and the recall-parity methodology.

Testing

  • Unit tests: vector conversions, parquet round-trip asserting pgvector.Vector reaches the driver, sharded-directory loading, multi-table schema validation/expansion, per-table data resolution, and smallint conversion/overflow on both paths.
  • End-to-end smoke test on 200k real rows (2 S3 shards): both backends loaded at 13–22k rows/sec, both indexes built (pg_search 49s, HNSW 99s), and a k6 run completed 108,999 queries with zero errors. Verified pgvector plans use the HNSW index and the pg_search KNN query returns correct rows.
  • Multi-table smoke test against paradedb v0.25.0: a two-table dataset (one table from CSV, one from a directory of parquet shards) loaded in one run, join query returned correct counts, drop removed both tables. smallint verified end-to-end from both CSV and parquet into real int2 columns (including bounds value 32767).

🤖 Generated with Claude Code

jamessewell and others added 3 commits September 1, 2026 20:12
schema.yaml now accepts a tables list (one entry per table with its own
columns) as an alternative to the single top-level table/columns form.
The loader resolves one data source per table under data/ with the same
preference order as before (<table>.parquet, <table>.csv, <table>/ shard
directory), loads tables sequentially, and drops every table. Validation
rejects mixed forms, nested entries, duplicates, and names the failing
table in per-backend column-type errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds smallint/int2 to both conversion paths (CSV string parsing and
parquet value mapping) with int16 bounds checks. Conversion happens in
the shared RowSource layer, so every backend receives a Go int16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jamessewell jamessewell changed the title feat: parquet loading, pgvector support, and cohere-10m vector dataset feat: parquet loading, pgvector support, multi-table datasets, and cohere-10m vector dataset Sep 3, 2026
jamessewell and others added 20 commits September 4, 2026 09:35
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vectors now flow through the shared row-source layer as neutral []float32;
the postgres driver wraps them into pgvector.Vector at insert (COPY codec),
and ES/OpenSearch marshal them as plain JSON arrays. The ES bulk path routes
a schema _id column to the document id (ES reserves _id in sources). Vector
schema validation now admits elasticsearch/opensearch.

cohere-10m gains an elasticsearch backend: dense_vector mapping (cosine,
float32 HNSW m=16/ef_construction=64 to mirror pgvector) with emb excluded
from _source, an ES service in the compose file, and a kNN scenario in
vector.js with ES_NUM_CANDIDATES as the recall knob.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stat-group and ingest-legend labels now read "paradedb 0.25.6" style,
extracted from the captured backend config (pg_search composite record,
PostgreSQL banner, or bare version string). Re-evaluated on every update
since config can arrive after the label is first rendered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared HTTP client caps requests at 15 minutes, which a _forcemerge
to one segment on a 44GB vector index exceeds (the merge completed
server-side; only the loader's post step reported failure). Operations
may now carry a "timeout" duration; cohere-10m's forcemerge gets 2h.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stdlib-only sweep of num_candidates against the held-out ground truth,
using the same kNN request shape as k6/vector.js. Ground truth ships as
JSON (query_id 1..100 in query_vectors.json row order) so the k6 host
needs no pyarrow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measured on the loaded 10m single-segment index: 40 candidates (the old
default) sits at 0.835 recall@10; 150 reaches 0.951. Full curve recorded
in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweeps paradedb.vector_cluster_max_probe running the exact k6 query shape
once per held-out vector via docker exec psql; same ground truth and
intersection logic as the ES sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
48g ES vs 8g pg containers would hand ES the whole index in page cache
while the others thrash. All three engines now get the same 8g cgroup
(ES heap 4g per its half-of-container guidance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1m slice of the Cohere corpus targeting a separate benchmark_1m database
so the 10m tables survive on the same containers. All containers capped
at the same 8g; both indexes fit fully in cache, complementing the 10m
data-larger-than-memory regime. Ships the 1m ground truth as JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pg_search registers its bm25 vector opclasses only when pgvector already
exists at install time; the old order left vector_cosine_ops undefined in
any fresh database.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same dense_vector mapping as 10m; ES scenario added to vector.js with
num_candidates env-tunable to the measured recall operating point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ES num_candidates=80 (0.955) vs paradedb max_probe=0.035 (0.958) against
the 1m ground truth; curves in the README. pgvector scenario removed from
vector.js (no HNSW index at 1m), ES stays unmerged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the paradedb.com performance-section shape — ~1%-selective full-text
filter ('battle') gating the kNN — mirroring upstream knn_top10_1pct.sql.
Both indexes now cover text (pdb unicode_words with english stemmer, ES
english analyzer, so the filter sets match); vector.js gains filtered
scenarios sharing a chart, with the pdb filtered probe on its own
connection string; recall scripts accept --filter-term and the 1pct
ground truth ships as JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 1m comparison runs the as-loaded multi-segment index (the calibrated
num_candidates=80 operating point is measured against it); merging to one
segment would silently change the geometry recall was tuned on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unfiltered re-measured after both rebuilds: ES num_candidates=95 (0.961)
vs pdb max_probe=0.035 (0.959). Filtered shape saturates at 1.000 on
both; cheapest saturation points pinned (ES 40, pdb 0.05).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text indexed on both engines (filtered shape), no ES force-merge,
vector.js ported from 1m (pdb + ES, filtered + unfiltered, VUS knob).
pgvector dropped from the 10m matchup as at 1m.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pdb max_probe=0.02 and ES num_candidates=20 both measure 0.999 on the
1%-filtered shape; the previous 1.000 points paid 2-2.5x the work for
one hit in a thousand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants