~/tools / Embeddings and search (RAG) / pgvector SQL Builder

pgvector SQL Builder — Write pgvector SQL: table, HNSW/IVFFlat index and nearest-neighbor query

Write the SQL to store and search embeddings in PostgreSQL with pgvector: a table with a vector(n) or halfvec(n) column, an HNSW or IVFFlat index with the operator class that matches your distance, and a nearest-neighbor query with a readable score. It checks the limits given in the pgvector documentation and estimates how much space the vectors take. Nothing leaves your browser.

Each vector takes 1,544 bytes, so 10,000 rows take about 15.4 MB for the vectors alone (indexes and other columns not included).

1. Extension and table

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE documents (
  id bigserial PRIMARY KEY,
  content text NOT NULL,
  embedding vector(384) NOT NULL
);

2. Index

CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

3. Search

PREPARE takes the vector of the question from /api/embed as $1. Pass it as text, such as '[0.01,-0.02,…]'.

PREPARE search_documents(vector(384)) AS
  SELECT id, content, 1 - (embedding <=> $1) AS cosine_similarity
  FROM documents
  ORDER BY embedding <=> $1
  LIMIT 5;

-- EXECUTE search_documents('[0.0123, -0.0456, ...]');

Limits and defaults from the pgvector 0.8.7 README and source code, checked on 2026-11-04. pgvector README

Everything is done in your browser. The tool writes SQL and connects to nothing.

pgvector is an open-source extension that adds vector similarity search to PostgreSQL, so the embeddings you make with Ollama can be stored in the same database as the rest of your data. A few details are easy to get wrong. The column must have the same number of dimensions as your embedding model. Each distance has its own operator: <=> for cosine distance, <-> for L2 distance, <#> for the negative inner product and <+> for L1 distance. An approximate index is only used when it was built with the operator class for the same distance, such as vector_cosine_ops for <=>, and when the query sorts by the operator itself in ascending order with a LIMIT. This tool writes the three parts from one set of choices: CREATE EXTENSION and CREATE TABLE, CREATE INDEX, and a PREPARE statement that returns the nearest rows with a readable score (cosine similarity is 1 minus the cosine distance, and the inner product is <#> multiplied by -1). It applies the limits in the pgvector README and source code for version 0.8.7: up to 16,000 dimensions in a column, indexes up to 2,000 dimensions for vector and 4,000 for halfvec, no L1 operator class for IVFFlat, HNSW m from 2 to 100, and ef_construction from 4 to 1,000 and at least 2 × m. With HNSW, a search returns at most hnsw.ef_search rows (40 by default), so the tool warns when LIMIT is larger. For IVFFlat it fills in lists and probes from the README's starting points: rows / 1000 for up to 1M rows, sqrt(rows) above that, and sqrt(lists) for probes. The generated SQL was run on pgvector 0.8.7 with PostgreSQL 16 using real 384-dimension vectors from Ollama, and EXPLAIN showed the index being used.

The core logic of this tool is open source under the MIT License. github.com/seigo5555/toolkitbox-lib/src/pgvector-sql.ts

Use Cases

  • Create the table for your document chunks before you load embeddings from Ollama into PostgreSQL
  • Check which operator goes with which operator class for cosine, L2, inner product or L1 distance, so that the index is actually used
  • Estimate how much space the vectors take on a small VPS, and compare vector with halfvec

Usage

  1. Enter the table name and the number of dimensions of your embedding model. This is the length of one vector from /api/embed; the Embedding Similarity Calculator on this site shows it.
  2. Choose the type, the distance and the index. Ollama's documentation recommends cosine similarity for most semantic search, and /api/embed returns vectors with a length of 1.
  3. Copy the three blocks and run them in psql or another client. Create an IVFFlat index after the data is loaded; an HNSW index can be created on an empty table.

FAQ

Which distance should I choose?

Ollama's embeddings documentation recommends cosine similarity for most semantic search use cases. In pgvector, <=> returns the cosine distance, and the tool turns it into cosine_similarity with 1 minus the distance. For vectors with a length of 1, like those from /api/embed, cosine distance, L2 distance and inner product put the rows in the same order, because for such vectors the squared L2 distance is 2 minus 2 times the cosine similarity.

HNSW or IVFFlat?

The pgvector README says HNSW has better query performance than IVFFlat (in terms of the speed-recall tradeoff) but builds more slowly and uses more memory, and that it can be created on an empty table because there is no training step. IVFFlat builds faster and uses less memory, and should be created after the table has some data. Without an index, pgvector does an exact search, which gives perfect recall.

Why does my query not use the index?

The README says the query needs ORDER BY and LIMIT, and the ORDER BY must be the distance operator itself in ascending order, not an expression such as 1 - (embedding <=> …) DESC. The operator must also match the operator class of the index: <=> with vector_cosine_ops, <-> with vector_l2_ops, <#> with vector_ip_ops and <+> with vector_l1_ops. The tool writes both from the same choice. The README also notes that a table scan may be faster when the table is small.

Why do I get fewer rows than LIMIT?

With HNSW, the results are limited by hnsw.ef_search, which is 40 by default; when we tried LIMIT 100 on a table of 200 rows, we got 40 rows until we raised hnsw.ef_search to 100. The README adds that filtering with WHERE is applied after the index is scanned, so a condition can leave even fewer rows. Since version 0.8.0, iterative index scans (SET hnsw.iterative_scan = strict_order or relaxed_order) scan more of the index when needed.

What is halfvec?

A half-precision vector type. Each vector takes 2 × dimensions + 8 bytes instead of 4 × dimensions + 8, and HNSW and IVFFlat indexes on halfvec support up to 4,000 dimensions instead of 2,000. Each element is a half-precision floating-point number, so the values are stored less precisely.

Is my data sent anywhere?

No. The tool only writes SQL in your browser and never connects to a database.

Related articles

Related