Loading issues
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).
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.