ybhnsw Can Index Wider Vectors Than PostgreSQL pgvector HNSW

When working with vector search, dimension limits matter.

Many embedding models fit comfortably inside 768, 1,024, or 1,536 dimensions. But some use cases need much wider vectors.

That raises an important question:

  • Can the database store the vector, and can it actually index it?

That distinction matters.

In pgvector, the vector type can support up to 16000 dimensions. However, PostgreSQL pgvector’s upstream hnsw index path for the vector type supports only up to 2000 indexed dimensions.

YugabyteDB’s ybhnsw index path can index a vector(16000) column.

Let’s prove it.

The big idea: PostgreSQL pgvector can store a vector(16000), but its upstream hnsw index path for vector tops out at 2000 dimensions. YugabyteDB’s ybhnsw index path can index a vector(16000).
Related tip: This demo focuses on the maximum indexed vector dimensions for hnsw and ybhnsw. For a deeper look at why HNSW indexes are so fast in YugabyteDB, including how the index is distributed and colocated with the table data at the tablet level, see Why HNSW Indexes Are So Fast in YugabyteDB .

What We Are Testing

This is a boundary test.

We are not benchmarking search latency, recall, or index build speed. We are simply proving the indexed dimension limit.

Database Index Method Test Expected Result
PostgreSQL + pgvector hnsw vector(2000) Index succeeds
PostgreSQL + pgvector hnsw vector(2001) Index fails
YugabyteDB ybhnsw vector(16000) Index succeeds
YugabyteDB N/A vector(16001) Column definition fails

Demo 1: PostgreSQL pgvector HNSW Limit

Run the following in PostgreSQL with pgvector installed:

				
					SELECT version();

CREATE EXTENSION IF NOT EXISTS vector;

SELECT extversion
FROM pg_extension
WHERE extname = 'vector';

DROP TABLE IF EXISTS tip_pg_hnsw_2000;
DROP TABLE IF EXISTS tip_pg_hnsw_2001;

CREATE OR REPLACE FUNCTION make_demo_vector(dim integer, seed integer DEFAULT 0)
RETURNS vector
LANGUAGE sql
IMMUTABLE
AS $$
  SELECT array_agg((((g.i + seed) % 97)::real / 97::real) ORDER BY g.i)::vector
  FROM generate_series(1, dim) AS g(i);
$$;
				
			

Create and index a vector(2000) column:

				
					CREATE TABLE tip_pg_hnsw_2000 (
    id        bigserial PRIMARY KEY,
    embedding vector(2000)
);

INSERT INTO tip_pg_hnsw_2000 (embedding)
SELECT make_demo_vector(2000, g.i)
FROM generate_series(1, 5) AS g(i);

DO $$
BEGIN
    EXECUTE '
        CREATE INDEX tip_pg_hnsw_2000_idx
        ON tip_pg_hnsw_2000
        USING hnsw (embedding vector_l2_ops)
    ';

    RAISE NOTICE 'PASS: PostgreSQL pgvector indexed vector(2000) with hnsw.';
EXCEPTION WHEN OTHERS THEN
    RAISE EXCEPTION 'FAIL: PostgreSQL pgvector should index vector(2000), but failed with: %', SQLERRM;
END $$;

SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'tip_pg_hnsw_2000';
				
			

Expected result:

				
					NOTICE:  PASS: PostgreSQL pgvector indexed vector(2000) with hnsw.
				
			

Now try one dimension higher: vector(2001).

				
					CREATE TABLE tip_pg_hnsw_2001 (
    id        bigserial PRIMARY KEY,
    embedding vector(2001)
);

INSERT INTO tip_pg_hnsw_2001 (embedding)
SELECT make_demo_vector(2001, g.i)
FROM generate_series(1, 5) AS g(i);

DO $$
BEGIN
    EXECUTE '
        CREATE INDEX tip_pg_hnsw_2001_idx
        ON tip_pg_hnsw_2001
        USING hnsw (embedding vector_l2_ops)
    ';

    RAISE EXCEPTION 'FAIL: PostgreSQL pgvector unexpectedly indexed vector(2001) with hnsw.';
EXCEPTION WHEN OTHERS THEN
    IF SQLERRM ILIKE '%dimension%' OR SQLERRM ILIKE '%2000%' THEN
        RAISE NOTICE 'PASS: PostgreSQL pgvector rejected hnsw on vector(2001): %', SQLERRM;
    ELSE
        RAISE;
    END IF;
END $$;
				
			

Expected result:

				
					NOTICE:  PASS: PostgreSQL pgvector rejected hnsw on vector(2001): ...
				
			
This proves the important PostgreSQL pgvector boundary:
  • vector(2000) + hnsw = succeeds
  • vector(2001) + hnsw = fails

Demo 2: YugabyteDB ybhnsw Limit

Now run the YugabyteDB version of the test:

				
					\set ON_ERROR_STOP on

SELECT version();

CREATE EXTENSION IF NOT EXISTS vector;

SELECT extversion
FROM pg_extension
WHERE extname = 'vector';

DROP TABLE IF EXISTS tip_yb_ybhnsw_16000;
DROP TABLE IF EXISTS tip_yb_ybhnsw_16001;

CREATE OR REPLACE FUNCTION make_demo_vector(dim integer, seed integer DEFAULT 0)
RETURNS vector
LANGUAGE sql
IMMUTABLE
AS $$
  SELECT array_agg((((g.i + seed) % 97)::real / 97::real) ORDER BY g.i)::vector
  FROM generate_series(1, dim) AS g(i);
$$;
				
			

Create and index a vector(16000) column with ybhnsw.

				
					CREATE TABLE tip_yb_ybhnsw_16000 (
    id        bigserial PRIMARY KEY,
    embedding vector(16000)
);

INSERT INTO tip_yb_ybhnsw_16000 (embedding)
SELECT make_demo_vector(16000, g.i)
FROM generate_series(1, 5) AS g(i);

DO $$
BEGIN
    EXECUTE '
        CREATE INDEX NONCONCURRENTLY tip_yb_ybhnsw_16000_idx
        ON tip_yb_ybhnsw_16000
        USING ybhnsw (embedding vector_l2_ops)
    ';

    RAISE NOTICE 'PASS: YugabyteDB indexed vector(16000) with ybhnsw.';
EXCEPTION WHEN OTHERS THEN
    RAISE EXCEPTION 'FAIL: YugabyteDB should index vector(16000) with ybhnsw, but failed with: %', SQLERRM;
END $$;

SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'tip_yb_ybhnsw_16000';
				
			

Expected result:

				
					NOTICE:  PASS: YugabyteDB indexed vector(16000) with ybhnsw.
				
			

You can also run a simple nearest-neighbor query against the table:

				
					SELECT id
FROM tip_yb_ybhnsw_16000
ORDER BY embedding <-> make_demo_vector(16000, 3)
LIMIT 3;
				
			

Now test one dimension higher: vector(16001).

				
					DO $$
BEGIN
    EXECUTE '
        CREATE TABLE tip_yb_ybhnsw_16001 (
            id        bigserial PRIMARY KEY,
            embedding vector(16001)
        )
    ';

    RAISE EXCEPTION 'FAIL: YugabyteDB unexpectedly allowed vector(16001).';
EXCEPTION WHEN OTHERS THEN
    IF SQLERRM ILIKE '%dimension%' OR SQLERRM ILIKE '%16000%' THEN
        RAISE NOTICE 'PASS: YugabyteDB rejected vector(16001): %', SQLERRM;
    ELSE
        RAISE;
    END IF;
END $$;
				
			

Expected result:

				
					NOTICE:  PASS: YugabyteDB rejected vector(16001): dimensions for type vector cannot exceed 16000
				
			
This proves the YugabyteDB boundary:
  • vector(16000) + ybhnsw = succeeds
  • vector(16001) = fails

Why This Matters

The difference is not just academic.

If your embedding model produces vectors wider than 2,000 dimensions, PostgreSQL pgvector may be able to store the vector, but the standard hnsw index path for the vector type cannot index it.

That means you may need to use workarounds such as:

  • ● reducing dimensions
  • ● using halfvec
  • ● binary quantization
  • ● indexing subvectors
  • ● storing the vector without an ANN index

With YugabyteDB’s ybhnsw, you can index a vector(16000) column directly.

Production reminder: This demo proves the indexed dimension ceiling. It does not prove that every 16,000-dimensional workload should use the same HNSW settings. For real workloads, test index build time, memory usage, query latency, recall quality, and write amplification with your actual data and query pattern.

Cleanup

				
					DROP TABLE IF EXISTS tip_pg_hnsw_2000;
DROP TABLE IF EXISTS tip_pg_hnsw_2001;
DROP TABLE IF EXISTS tip_yb_ybhnsw_16000;
DROP TABLE IF EXISTS tip_yb_ybhnsw_16001;

DROP FUNCTION IF EXISTS make_demo_vector(integer, integer);
				
			

What to Remember

PostgreSQL pgvector and YugabyteDB can both expose familiar pgvector-style SQL, but their HNSW indexing limits are not the same.

For the vector type:

  • ● PostgreSQL pgvector hnsw: 2,000 indexed dimensions
  • ● YugabyteDB ybhnsw: 16,000 indexed dimensions

So the practical takeaway is simple:

  • If your application needs HNSW indexing on vectors wider than 2,000 dimensions, YugabyteDB’s ybhnsw gives you a much larger indexed-dimension ceiling.

Have Fun!

Every Wednesday, we get a company-sponsored DoorDash credit, and one of my favorite spots is Frank’s North Hills Home Base. They have a loyal following… and for good reason. The buffalo chicken pizza keeps people coming back!

Since I’m moving to Dallas, I’m guessing they won’t deliver quite that far, so this may have been my last visit for a while. 🍕😢