Who this is for and what you will be able to do
This article is for developers who already run Postgres and want to add semantic search, meaning search by meaning rather than by exact words, to their own content. After reading it you should be able to install pgvector, choose a vector type and a distance operator, create an HNSW index, and understand the setup we use for TellWorld's question-answering feature, including how we avoid re-embedding text that has not changed.
What pgvector is
pgvector is an extension that adds vector types and nearest-neighbor search to Postgres. It provides vector, halfvec, bit and sparsevec types. The halfvec type stores half-precision elements. It takes 2 * dimensions + 8 bytes, against 4 * dimensions + 8 bytes for vector, and both support up to 16,000 dimensions.[2]
By default pgvector performs exact nearest-neighbor search, which has perfect recall. Adding an HNSW or IVFFlat index trades some recall for speed.[2] An HNSW index builds a multilayer graph. Its query performance is better than IVFFlat's, but it takes longer to build and uses more memory. It can be created before any data is loaded, because there is no training step.[2] We use HNSW.
Several distance operators are available. <-> is L2 distance, <#> is the negative inner product, <=> is cosine distance, <+> is L1 distance, <~> is Hamming distance for binary vectors and <%> is Jaccard distance for binary vectors. The README explains that <#> returns the negative inner product because Postgres supports only ASC order index scans on operators.[2]
Preparation
The README compiles and installs the extension from source at version v0.8.7. The Linux and Mac source install supports Postgres 13+.[2] The CHANGELOG lists 0.8.7 (2026-10-01) as the latest release and 0.9.0 (unreleased) as the next version.[3] Pin the version you install and read the CHANGELOG before you upgrade.
For a Docker setup, the image pgvector/pgvector:pg18-trixie adds pgvector to the official Postgres image.[2] With Homebrew Postgres, brew install pgvector adds it only to the postgresql@18 and postgresql@17 formulas.[2] The README also documents pgxn install vector for PGXN, sudo apt install postgresql-18-pgvector for APT, sudo yum install pgvector_18 for Yum, apk add postgresql-pgvector for APK, and conda install -c conda-forge pgvector for conda-forge.[2]
For embeddings we use Alibaba Cloud Model Studio, known in Chinese as 阿里云百炼. Its text-embedding-v4 model is a multilingual text vector model trained from Qwen3. Its user-defined dimensions range from 64 to 2048.[4] The Beijing rate limits are 1800 RPM and 1,200,000 TPM.[4] The model is billed by input tokens, and output is not charged.[6] In Beijing the input price is 0.5 yuan per million tokens, Batch calls are half price, and there is a free quota of 1 million tokens valid for 90 days.[5][6]
Step by step
- Install pgvector from source. The README's commands are these:[2]
cd /tmp
git clone --branch v0.8.7 https://github.com/pgvector/pgvector.git
cd pgvector
make
make install # may need sudo- Enable the extension once in each database where you want to use it, with
CREATE EXTENSION vector;.[2]
- Create an HNSW index. The README's example, on a table it calls
items, isCREATE INDEX ON items USING hnsw (embedding vector_l2_ops);. The optionsmandef_constructiondefault to16and64, and you can set them withWITH (m = 16, ef_construction = 64).[2] HNSW indexes are created per distance function. The README listsvector_ip_ops,vector_cosine_ops,vector_l1_ops,bit_hamming_opsandbit_jaccard_opsas well, and it says to usehalfvec_l2_opsforhalfvec.[2] We usehalfvec_cosine_ops, and the matching operator for the query is<=>.[2]
- Query with an
ORDER BYon the distance operator, in ascending order, plus aLIMIT. An index can only be used when the query has both.[2] The search candidate list is controlled byhnsw.ef_search, which is40by default. Set it per transaction withSET LOCAL:[2]
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT ...
COMMIT;- Choose the dimension and batch size of the embedding model.
text-embedding-v4defaults to1024dimensions, and the Beijing table lists2,048,1,536,1,024,768,512,256,128and64as options.[5] The batch size is10, so one request can vectorize up to 10 texts, and each text must not exceed8192tokens.[5] The Beijing table gives a maximum of33,000tokens per batch, while the Singapore table gives8,192.[5]
- Point the client at the OpenAI-compatible endpoint. The Beijing
base_urlishttps://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1. The Singapore region useshttps://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1. The key is read from theDASHSCOPE_API_KEYenvironment variable.[5]
Our practice
Every entry on TellWorld is split into chunks, stored in a cms_chunk table. Each row holds the text, a token count, a content hash, the name of the model and the embedding.[1] The embedding column is halfvec(1024), indexed with HNSW using halfvec_cosine_ops.[1] The 1024 matches the default dimension of text-embedding-v4.[5] The same table has a trigram GIN index on the text, using gin_trgm_ops, for keyword matching.[1]
Embeddings come from an OpenAI-compatible /embeddings endpoint, with text-embedding-v4 as the default model.[1] Only chunks whose content changed are re-embedded. The content hash is what makes that possible. On 13 November 2026 a run covered 5,342 documents and 34,228 chunks, and it embedded 94 chunks using 7,531 tokens.[1] The question-answering feature on the site retrieves from these chunks.[1]
Recording the model name in each row matters when the model changes, because it tells you which model produced each vector.[1] This is our design for our content. The number of chunks and the tokens used come from our own run, and another site will see different numbers.
Limits and common problems
- With an approximate index, the filter is applied after the index is scanned. If a condition matches 10% of rows, then with HNSW and the default
hnsw.ef_searchof 40, only about 4 rows will match on average.[2] - For HNSW, a query can return fewer rows than you asked for when a filter removes too many candidates. The README documents iterative index scans, introduced in
0.8.0, which you enable withSET hnsw.iterative_scan = strict_order;.[2] - The README recommends
CREATE INDEX CONCURRENTLYin production to avoid blocking writes.[2] NULLvectors are not indexed, and neither are zero vectors for cosine distance.[2]- The HNSW limits are
2,000dimensions forvector,4,000forhalfvec,64,000forbitand1,000non-zero elements forsparsevec.[2] - IVFFlat is the other option. You should create the index after the table has some data. Choose
listsatrows / 1000for up to 1M rows andsqrt(rows)for more than 1M rows, and startprobesatsqrt(lists). The README sets probes withSET ivfflat.probes = 10;.[2] - Batch calls to the embedding API cost half as much, but the batch limits still apply. The embedding documentation recommends
text-embedding-v4with OpenAI-compatible Batch calls for large-scale non-real-time text, to lower cost significantly.[5] - The
halfvecindex limit of4,000dimensions and our1024dimensions are well within the HNSW limits.[2]
Next steps
Read the pgvector README[2] and the CHANGELOG.[3] For the embedding model, read the Model Studio embedding documentation[5] and the billing page.[6] Before you ship, run one re-embedding pass over a small set of documents and compare the token counts with the billing page.
Sources
- 1
- 2pgvector README (master)pgvector (official GitHub repository)
- 3pgvector CHANGELOG (master)pgvector (official GitHub repository)
- 4text-embedding-v4 模型信息阿里云百炼 (Alibaba Cloud Model Studio)
- 5向量化(Embedding)阿里云百炼 (Alibaba Cloud Model Studio)
- 6模型计费 (billing-for-model-studio)阿里云百炼 (Alibaba Cloud Model Studio)
Cite this page: Semantic search with Postgres and pgvector. TellWorld, https://www.tellworld.net/practice/practice-pgvector-search. Updated 2026-11-14