Follow-up to PAR-227 (conversational ANSWER). The bot's retrieval is purely lexical today — PostgresTaskSystem.searchTasks (V12 search_tsv tsvector + ts_rank, OR-prefix, ILIKE fallback). It misses conceptually-related issues that don't share words ("can't pay rent" vs "rent payment fails"). Add semantic search via embeddings so search_tasks (and thus FIND + the ANSWER action) retrieves by meaning, and fuse it with the existing full-text for the best of both.
PostgresTaskSystem.searchTasks(query, limit) (src/main/java/io/paradaux/tesks/tasks/store/PostgresTaskSystem.java). The agentic loop's search_tasks tool (TriageService.runReadTool) and applyFind/applyAnswer all funnel through it — improving it lifts every path with no caller changes.com.openai:openai-java:4.37.0) — client.embeddings(). Mirror the LlmClient/OpenAiLlmClient port pattern: add an EmbeddingClient port + OpenAI adapter, configured under tesks.llm (api-key/base-url reused) with tesks.llm.embedding-model (default text-embedding-3-small, 1536 dims). Meter tokens through the existing LlmUsageSink.postgres:16 (gitops/apps/postgres/base/resources.yaml) — no pgvector. Switch that image to pgvector/pgvector:pg16 (or install the extension) and CREATE EXTENSION IF NOT EXISTS vector in the tesks database. This is a shared instance (also hosts paradaux-api's apps db), so validate the image swap doesn't disrupt other databases. Gitops change + Argo sync.V28__issue_embeddings.sql (latest is V27): create extension if not exists vector; add issue.embedding vector(1536) (nullable — NULL = not yet embedded), plus an HNSW index using hnsw (embedding vector_cosine_ops). Keep search_tsv — this is additive.OpenAiEmbeddingClient adapter (io.paradaux.tesks.llm): float[] embed(String text) / batch variant; disabled-safe when no api-key (like OpenAiLlmClient).title + "\n" + description on create and on title/description update (createTask/updateTask in PostgresTaskSystem). Do it best-effort/async so a failed/slow embedding never blocks the task write (NULL embedding just means it's not yet semantically searchable; full-text still covers it).searchTasks — embed the query, ANN by cosine distance, and fuse with the existing ts_rank lexical results (Reciprocal Rank Fusion is simplest/robust). Tenant-scope the ANN (where tenant_id = ? and archived_at is null and embedding is not null). Fall back to pure full-text when embeddings are disabled or the query embed fails. Keep the Task projection unchanged.semantic_search tool to the agent in addition to keyword search_tasks, or just make search_tasks hybrid (preferred — fewer tools, the model already uses it well).text-embedding-3-small is cheap; one embed per query + one per issue write. Cache the query embedding within an agentic loop if the same query repeats.tenant_id predicate (IIDOR parity with the rest of PostgresTaskSystem).issue table in the tesks DB, not the tesks-ui Postgres mirror.EmbeddingClient so the test is deterministic — no live API).Reconciled 2026-10-13: the 2026-06-18 comment records an uncommitted working-tree implementation, not a shipped feature. Current merged main has no EmbeddingClient, EmbeddingMaintainer, vector migration, or hybrid-search implementation. Its migrations have since advanced through V31, so the proposed V28/V29 numbering and old package paths must be revised. Status remains In Progress pending recovery/verification of that earlier work; no claim of deployment or completion is made.
Supersedes the 2026-10-13 assessment: PR #12 merged on 2026-10-14. It implements hybrid lexical/vector retrieval, asynchronous embedding maintenance and resumable backfill, tenant isolation, fallback, metering, and V32. The PR records 244 passing backend tests and pgvector verified in both dev and production after GitOps #54. Current main retains RankedSearchIssues, EmbeddingMaintainer and the embedding persistence implementation. Marked Done for completed implementation. Embedding calls are opt-in; this reconciliation does not assert that production enablement or historical backfill has been executed.
Implementation landed in the working tree (tesks + gitops), compiles clean (compileJava + compileTestJava on JDK 21). Two premises in this ticket were stale and corrected:
apps/postgres:16Deployment. Thedata/postgresCNPG clusterpg(operator chart 0.28.2 = CNPG 1.27, PostgreSQL 16) hoststesks-dev/tesks-prod. So pgvector is a CNPG concern.pgvector enablement (PG16 ⇒ no PG18 image-volume path):
gitops/data/postgres/image/Dockerfile: bundled operand image (FROM ghcr.io/cloudnative-pg/postgresql:16 + postgresql-16-pgvector).cluster.yaml:imageName→ harbor.paradaux.io/paradaux-public/postgresql-pgvector:16.databases.yaml:Database.spec.extensions: [{name: vector}]on tesks-dev/tesks-prod — operator runs CREATE EXTENSION as superuser (the tesks role can't; the V29 migration deliberately doesn't).App layer (route-independent):
Remaining (needs cluster/Docker, not code): build+push the pgvector operand image to Harbor, manual-sync data/postgres (HA rolling restart of shared
pg), run./gradlew testwhere Docker is available, then commit to develop.