diff --git a/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java b/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java index e499a23..4e69124 100644 --- a/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java +++ b/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java @@ -930,7 +930,9 @@ public class DatabaseInitConfig { /** * 为 vector_store 表添加全文检索支持(tsvector 列 + GIN 索引 + 触发器) - * 幂等操作:已存在则跳过 + * 全幂等自愈:每一步都用幂等 SQL(ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS / + * CREATE OR REPLACE + DROP TRIGGER IF EXISTS),不做「列已存在就 return」的早退, + * 避免半途失败或触发器被手动删除后新文档 content_tsvector 恒为 NULL 的静默缺口。 */ private void initVectorStoreFullTextSearch() { try { @@ -940,19 +942,12 @@ public class DatabaseInitConfig { return; } - // 检查 content_tsvector 列是否已存在 - String checkSql = "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'vector_store' AND column_name = 'content_tsvector'"; - Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class); - if (count != null && count > 0) { - return; // 已初始化 - } - - log.info("为 vector_store 添加全文检索支持"); + log.info("初始化 vector_store 全文检索支持"); - // 添加 tsvector 列 - jdbcTemplate.execute("ALTER TABLE vector_store ADD COLUMN content_tsvector tsvector"); + // 添加 tsvector 列(已存在则跳过) + jdbcTemplate.execute("ALTER TABLE vector_store ADD COLUMN IF NOT EXISTS content_tsvector tsvector"); - // 填充已有数据 + // 回填已有数据(幂等:只补 NULL 行) jdbcTemplate.execute("UPDATE vector_store SET content_tsvector = to_tsvector('simple', coalesce(content, '')) WHERE content_tsvector IS NULL"); // GIN 索引 diff --git a/src/main/java/com/wok/supportbot/rag/HybridSearchService.java b/src/main/java/com/wok/supportbot/rag/HybridSearchService.java index d7fc7ae..6d6c24e 100644 --- a/src/main/java/com/wok/supportbot/rag/HybridSearchService.java +++ b/src/main/java/com/wok/supportbot/rag/HybridSearchService.java @@ -97,6 +97,9 @@ public class HybridSearchService { * 全文关键词检索 * 使用 PostgreSQL tsvector 全文索引,按 ts_rank 排序。 * P1-2.1: 始终过滤禁用的文档(metadata->>'enabled' = 'true') + * 注意:vector_store 为物理删除(无 is_delete 列),仅保证有 + * id / content / metadata / embedding / content_tsvector / create_time 六列, + * 禁止在原生 SQL 中引用 is_delete、update_time 等业务表字段。 */ private List keywordSearch(String query, int topK, List categoryIds) { String categoryFilter = buildCategoryFilter(categoryIds); @@ -105,7 +108,6 @@ public class HybridSearchService { ts_rank(content_tsvector, plainto_tsquery('simple', ?)) AS rank FROM vector_store WHERE content_tsvector @@ plainto_tsquery('simple', ?) - AND is_delete = false AND metadata->>'enabled' = 'true' %s ORDER BY rank DESC diff --git a/src/main/resources/init-database.sql b/src/main/resources/init-database.sql index 24349ec..4806813 100644 --- a/src/main/resources/init-database.sql +++ b/src/main/resources/init-database.sql @@ -367,6 +367,12 @@ CREATE TABLE IF NOT EXISTS vector_store ( update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); +-- ⚠️ 与 Spring AI 自动建表的差异(勿误判): +-- 1. Spring AI PgVectorStore 自动建表时 metadata 是 json(非 jsonb),且不含 create_time/update_time。 +-- create_time/content_tsvector 由 DatabaseInitConfig 补加;update_time 仅本手动脚本含、无代码引用。 +-- 2. vector_store 为物理删除(pgVectorVectorStore.delete),没有 is_delete 列。 +-- 任何原生 SQL 禁止引用 is_delete / update_time,只允许 id/content/metadata/embedding/content_tsvector/create_time。 + -- HNSW 向量索引(余弦距离) CREATE INDEX IF NOT EXISTS idx_vector_store_embedding ON vector_store USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);