"""pgvector 索引管理 — LlamaIndex PGVectorStore 封装。 Schema 隔离:使用显式表名 `rag_platform_kb_chunks`,不可使用默认 `data_`。 确认 `create_if_not_exists` 不会触碰 `episodic_memory` 或任何 `memory/` 所属表。 """ from __future__ import annotations import logging from typing import TYPE_CHECKING logger = logging.getLogger(__name__) if TYPE_CHECKING: from llama_index.vector_stores.postgres import PGVectorStore # 显式表名 — 与 memory/episodic.py 的 episodic_memories 表完全隔离 KB_CHUNKS_TABLE = "rag_platform_kb_chunks" # 默认 embedding 维度(OpenAI text-embedding-3-small) DEFAULT_EMBED_DIM = 1536 def _async_to_sync_url(database_url: str) -> str: """将 asyncpg URL 转换为 psycopg2 URL(LlamaIndex PGVectorStore 使用同步驱动)。""" return database_url.replace("postgresql+asyncpg://", "postgresql://") def create_vector_store( database_url: str, embed_dim: int = DEFAULT_EMBED_DIM, hybrid_search: bool = True, ) -> "PGVectorStore": """创建 PGVectorStore,使用显式表名实现 schema 隔离。 Args: database_url: SQLAlchemy 数据库 URL(async 或 sync 均可) embed_dim: embedding 向量维度 hybrid_search: 是否启用混合搜索(向量 + 全文) Returns: LlamaIndex PGVectorStore 实例 Raises: ImportError: 如果 llama-index-vector-stores-postgres 未安装 """ from llama_index.vector_stores.postgres import PGVectorStore sync_url = _async_to_sync_url(database_url) logger.info( "Creating PGVectorStore: table=%s, embed_dim=%d, hybrid=%s", KB_CHUNKS_TABLE, embed_dim, hybrid_search, ) return PGVectorStore.from_params( database=sync_url, table_name=KB_CHUNKS_TABLE, embed_dim=embed_dim, hybrid_search=hybrid_search, text_search_config="english", # U4 将用 jieba 替换为中文分词 # ponytail: 不设 schema_name,默认 public — 避免创建独立 schema 的运维复杂度 # 表名前缀 rag_platform_ 已足够隔离 ) async def ensure_vector_store_schema(database_url: str, embed_dim: int = DEFAULT_EMBED_DIM) -> None: """确保 vector store 表存在(幂等)。 在应用启动时调用,创建表结构(如果不存在)。 不会触碰 episodic_memory 或任何 memory/ 所属表。 """ vs = create_vector_store(database_url, embed_dim=embed_dim) # PGVectorStore.__init__ 内部会调用 create_if_not_exists # 显式调用 _initialize 来确保表创建 vs._initialize() logger.info("Vector store schema ensured: table=%s", KB_CHUNKS_TABLE)