mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
The original rejection of pgvector was tested rather than reasoned, and it stands: pgvector/pgvector:pg18 means musl to glibc on an existing data volume, and 44 collation-dependent indexes — 18 of them UNIQUE — are then walked with the wrong comparator, silently, because musl records no collation version so Postgres's own mismatch check never fires. So the extension is compiled INTO the alpine image instead. libc never changes, and there is nothing to rebuild. Verified end to end rather than argued: an existing volume migrated on stock alpine, stopped, restarted on the pgvector image, then bt_index_check(heapallindexed => true) across 175 btree indexes — zero errors. Two build flags carry more weight than they look. OPTFLAGS="" because pgvector's Makefile defaults to -march=native, which bakes the build host's instruction set into vector.so and gives SIGILL mid-query on a host without it, with an empty log. with_llvm=no because a missing .bc file makes the JIT skip inlining silently, and inlining does not apply to index scans anyway. Pinned at v0.8.6, not the earliest that works. v0.8.0 does not compile against Postgres 18 at all (vacuum_delay_point gained a parameter); v0.8.1 is the first that does. But 0.8.3 fixed possible index corruption with HNSW vacuuming and 0.8.4 fixed an unrepaired HNSW graph, and message_embedding is ON DELETE CASCADE from message on a system that deletes mail in bulk — so vacuum is a hot path here and shipping the earliest that merely compiles would ship two known corruption bugs. THE HANDBOOK'S 24x WAS WRONG AND IS CORRECTED TO 1.75x. The SIMD dot product is microseconds; what dominates is that embedding is real[], so every row pays a 4KB detoast and a fresh real[] to vector conversion. 74,000 rows: 7.70s becomes 4.51s. NO INDEX IS CREATED, and that is the finding rather than an omission. An HNSW index answers ORDER BY ... LIMIT k and the search asks distance <= 0.45, which no ANN index serves. The expression form needed to build one at all is (embedding::vector(1024)), which Postgres treats as a DIFFERENT expression from the inlined function's (embedding::vector) — so the index exists, the planner ignores it, and nothing says so. And it costs 7m40s and 578MB to build at the maintenance_work_mem this stack ships, behind the one advisory lock every entrypoint waits on: an eight-minute dead stack, not a slow migration. The measured route to an index is written down in ai-assist.md rather than half taken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
3.7 KiB
YAML
66 lines
3.7 KiB
YAML
# pgvector, on the database image this stack already runs. Opt in with:
|
|
#
|
|
# docker compose -f compose.yaml -f compose.pgvector.yaml up -d --wait
|
|
#
|
|
# and keep both -f flags on every later `up`, `pull` and `down`, or compose goes
|
|
# back to the stock image on the next command.
|
|
#
|
|
# WHY THIS IS AN OVERLAY AND NOT A LINE IN compose.yaml
|
|
# ────────────────────────────────────────────────────
|
|
# pgvector is not required and must never become required. The plpgsql distance
|
|
# function in Version20260826210000 is the shipped path and stays correct; this
|
|
# only makes it faster. Folding the build into compose.yaml would make every
|
|
# `git clone && docker compose up -d` compile a C extension before the database
|
|
# could start, for a feature most installs never switch on — and would take the
|
|
# database off a pinned upstream image for people who never asked for AI at all.
|
|
#
|
|
# The layering matches compose.demo.yaml: this goes on top of compose.yaml, not
|
|
# on top of compose.prod.yaml. It touches one service and changes nothing else.
|
|
#
|
|
# WHY IT IS BUILT LOCALLY RATHER THAN PULLED
|
|
# ──────────────────────────────────────────
|
|
# `pgvector/pgvector:pg18` exists and is the wrong answer here. It is Debian and
|
|
# the shipped image is Alpine, so pulling it swaps musl for glibc UNDER AN
|
|
# EXISTING DATA VOLUME. That was tested rather than reasoned about: index scans
|
|
# return wrong rows, with an empty log, because musl records no collation
|
|
# version — `datcollversion` is NULL, Postgres's mismatch check never fires, and
|
|
# 44 collation-dependent indexes, 18 of them UNIQUE, are walked with the wrong
|
|
# comparator. docs/install/upgrading.md says "Starting the new image IS running
|
|
# it", so a libc change may never arrive by pulling a tag.
|
|
#
|
|
# docker/postgres/Dockerfile builds FROM postgres:${POSTGRES_VERSION}-alpine and
|
|
# adds three files. Same libc, same server binaries, same on-disk format —
|
|
# nothing that compares existing data changes, so there is no index to rebuild
|
|
# and no dump-and-restore to schedule. That file carries the full reasoning.
|
|
#
|
|
# The build is about 30 seconds once and then cached, and adds ~1 MB to a 304 MB
|
|
# image.
|
|
|
|
# The one place the image is named, and the args that decide what is in it.
|
|
#
|
|
# The tag carries BOTH versions on purpose. A locally built image is not pulled
|
|
# and not digest-checked, so a tag that said only `18` would go on pointing at
|
|
# whatever was built first: bumping PGVECTOR_VERSION would rebuild the layers,
|
|
# retag nothing an already-created container refers to, and leave the operator
|
|
# looking at the old extension while `docker compose config` swore otherwise.
|
|
x-pgvector-database: &pgvector-database
|
|
image: plmail/postgres-pgvector:${POSTGRES_VERSION:-18}-${PGVECTOR_VERSION:-v0.8.6}
|
|
build:
|
|
context: ./docker/postgres
|
|
args:
|
|
# Interpolated from the same variable compose.yaml uses for the stock
|
|
# image, so POSTGRES_VERSION=19 still moves the whole stack together
|
|
# instead of compiling against 18's headers and loading into 19.
|
|
POSTGRES_VERSION: ${POSTGRES_VERSION:-18}
|
|
# v0.8.1 is the earliest tag that compiles against PostgreSQL 18 — 0.8.0
|
|
# fails on vacuum_delay_point()'s new parameter. This is pinned three
|
|
# releases later because 0.8.3 and 0.8.4 fixed HNSW index corruption and
|
|
# a broken graph repair during vacuum, and message_embedding is vacuumed
|
|
# constantly: it hangs off `message` with ON DELETE CASCADE and mail is
|
|
# deleted in bulk. docker/postgres/Dockerfile spells that out.
|
|
PGVECTOR_VERSION: ${PGVECTOR_VERSION:-v0.8.6}
|
|
|
|
services:
|
|
database:
|
|
<<: *pgvector-database
|