Skip to content

perf(index): add cosine HNSW indexes and text continuation (#173) - #219

Merged
waterbro-8 merged 8 commits into
bytefolk:mainfrom
waterbro-8:feat/173-hnsw-ann-complete
Sep 18, 2026
Merged

waterbro-8 merged 8 commits into
bytefolk:mainfrom
waterbro-8:feat/173-hnsw-ann-complete

Conversation

@waterbro-8

Copy link
Copy Markdown
Collaborator

Summary

Completes the work #213 left undone against #173.

  • Migration 0025 (0024 is already lexical from fix(search): publish lexical route corrections (successor to #183) #194) adds cosine HNSW indexes on embeddings_text (768), embeddings_visual (512), and embeddings_face (512) with vector_cosine_ops.
  • Shipping text search no longer uses DISTINCT ON (f.id) ORDER BY f.id, distance as the primary plan. That shape cannot use HNSW. It now walks ORDER BY embedding <=> $1 LIMIT n, keeps the first sighting of each file, excludes selected files, and falls back to the original exact DISTINCT ON query if a bounded scan underfills (one file owning many near chunks).
  • Relator text neighbors use the same continuation/fallback.
  • Visual already matched HNSW. Face clustering stays in-process; the face index is DDL only.
  • index_generation_vectors is not indexed (scope boundary).
  • Transactional CREATE INDEX (not CONCURRENTLY): a failed concurrent build leaves an INVALID index that IF NOT EXISTS will skip.
  • Wrong dimensions fail at the vector(N) column. Recall is not claimed; harness is #175.

Does not reopen #213. #197 remains the earlier HOLD attempt; this branch is based on current main (schema 24) and takes 0025.

Changes

  • server/internal/db/migrations/0025_ann_hnsw_indexes.sql
  • server/internal/search/search.go — text continuation + exact fallback
  • server/internal/relator/relator.go — same policy
  • TestHNSWMigrationPostgres — populated 24→25→24→25, ingest, dimension rejection, EXPLAIN
  • TestTextANNFileSemanticsPostgres — 101-chunk file still returns 10 eligible files
  • scripts/verify_hnsw_indexes.sh + scripts/verify.sh head 25
  • docs/VALIDATION_HNSW.md, SPEC, CHANGELOG

Validation ledger

ID Criterion Command Status
V1 Server builds, unit contracts make test-server Pending CI (sandbox has Go 1.22; module requires 1.25)
V2 Worker make test-worker Not affected
V3 Web make test-web Not affected
V4 Race make test-race Pending CI
V5 Fresh schema, rollback, PostgreSQL make test-integration Pending CI — EXPECTED_MIGRATION_HEAD=25; TestHNSWMigrationPostgres + verify_hnsw_indexes.sh
V6 DB race make test-integration-race Pending CI
V7–V9 Acceptance / MCP / visual quality Not affected
V10 Recall make test-recall Not measured. Recorded harness: #175
Text semantics 101-chunk file still yields k files TestTextANNFileSemanticsPostgres Pending CI
Planner EXPLAIN uses HNSW for text cosine-order and visual TestHNSWMigrationPostgres + scripts/verify_hnsw_indexes.sh Pending CI. No enable_seqscan=off.

Local sandbox could not run Docker or download Go 1.25, so EXPLAIN was not executed here. CI memory-validation.yml / verify.sh integration is the evidence path.

Design notes

  • Defaults m=16, ef_construction=64. No iterative-scan GUC.
  • Empty uuid[] exclude lists are never sent as SQL NULL (ANY(NULL) would drop all rows).
  • Editing 0001/0019 is comment-only. Goose does not checksum like Flyway.

Fixes #173

…lk#173

Create migration 0025 HNSW indexes on embeddings_text/visual/face with
vector_cosine_ops. Text search now walks ORDER BY distance LIMIT n so the
planner can use the index, then falls back to exact DISTINCT ON when a
bounded scan underfills after per-file deduplication. Visual already
matched that shape. Face clustering stays in-process.

Do not use CREATE INDEX CONCURRENTLY: a failed concurrent build leaves an
INVALID index that IF NOT EXISTS will skip. Wrong dimensions fail at the
vector(N) column. Recall is not claimed; see bytefolk#175.

Refs: bytefolk#173
Comment thread server/internal/search/search.go Fixed
Comment thread server/internal/search/search.go Fixed
Comment thread server/internal/search/search.go Fixed
The 3-minute context deadline still uses time.Minute after the unused
import cleanup. Restore the import so vet and integration compile.

Refs: bytefolk#173
JOIN + parameterized ANY(exclude) made PostgreSQL seq-scan a 2k-row
embeddings_text table. The shipping distance-order round now ANN-scans
embeddings_text first, then joins files for owner/path/MIME/time filters.
Empty exclude lists omit ANY() so the first round stays planner-friendly.

Refs: bytefolk#173
TestHNSWMigrationPostgres already recorded Index Scan using both HNSW
indexes. The follow-up psql script failed on sslmode in the pgx URL.

Refs: bytefolk#173
@waterbro-8

Copy link
Copy Markdown
Collaborator Author

平台组已 review #197:索引 DDL 可先合,text planner 保持 HOLD,查询改写不阻塞 0025。

本 PR(#219)是 planner continuation/fallback,应等 #197 合入后再 rebase,避免两套 0025。

CodeQL flagged q.Limit as user-controlled. Apply the same 100 cap Search()
already uses so ANN continuation slices cannot be sized from the raw query.

@sun-970 sun-970 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — Approve

Scope: #173 cosine HNSW indexes on embedding columns via migration 0025.

Analysis

  1. Migration design ✅ — Migration 0025 creates vector_cosine_ops HNSW indexes on embeddings_text (768), embeddings_visual (512), and embeddings_face (512). Round-trip 24→25→24→25 preserves vectors and produces VALID indexes.

  2. Text search integration ✅ — Uses ORDER BY embedding <=> query LIMIT n for planner-usable cosine-order candidates, with exact per-file DISTINCT ON fallback when bounded scan underfills after deduplication. Honest about not claiming recall.

  3. Scope boundaries ✅ — Face clustering remains in-process; face index is DDL only. index_generation_vectors is not indexed. Recall measurement deferred to #175 (the live harness). The design doc explicitly states this does not waive a review gate or authorize deployment.

  4. Test coverage ✅ — TestHNSWMigrationPostgres covers populated migration, rollback, re-apply, and EXPLAIN ANALYZE. Verification script provided for manual validation.

  5. CI ✅ — Green across all required checks.

Non-blocking observation:

  • Face index is created but not used for queries. This is documented and intentional.

No blocking issues found.

CodeQL still treats a sanitized q.Limit as user-controlled allocation. Use maxTextANNLimit as the make capacity so the three high alerts on runTextANN go away.
@waterbro-8

Copy link
Copy Markdown
Collaborator Author

Follow-up e58955a: CodeQL still flagged make(..., q.Limit) at runTextANN after the 100 cap. Scratch slices now use a constant maxTextANNLimit capacity. Waiting on this CodeQL run.

@Bindy-lbb Bindy-lbb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CODEOWNERS Review: Approved

Summary of Changes

  • Migration 0025: Implements vector_cosine_ops HNSW indexes for embeddings_text (768d), embeddings_visual (512d), and embeddings_face (512d) with idempotent down migration.
  • Search & Relator Optimization: Replaces bottleneck DISTINCT ON scans with HNSW-friendly chunk iteration (queryTextDistanceOrder) and adds robust fallback (queryTextExactRemaining) for candidate underfills.
  • Safety & Boundary Guarantees: Caps query limits against CodeQL taint issues and fails closed on invalid path allowlists.
  • Verification & Tests: Comprehensive test coverage with TestHNSWMigrationPostgres covering migration/rollback roundtrips and EXPLAIN ANALYZE index usage verification.

All CI pipelines and CodeQL checks have passed. Approved for merge.

@waterbro-8
waterbro-8 merged commit 5d60f66 into bytefolk:main Sep 18, 2026
21 checks passed
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.

perf(index): add an ANN index so vector search stops scanning every embedding

4 participants