一、背景
随着企业客服场景的智能化升级,传统基于关键词匹配的客服系统,已难以满足用户对语义理解和多模态检索的需求。
本方案基于 PostgreSQL 15 + pgvector 0.8.2扩展,构建了一套智能客服系统,支持文本与图片的向量化检索,并利用余弦相似度(Cosine Similarity)实现高精度的语义匹配,从而提升客服系统的智能化水平与用户体验。
二、目录
一、环境检测与扩展安装
二、清理旧表
三、创建核心表结构
四、创建辅助索引
五、插入智能客服示例数据
六、统计信息更新
七、创建 HNSW 向量索引
八、搜索参数配置
九、向量检索功能验证
十、EXPLAIN ANALYZE 索引验证
十一、运维监控 SQL
十二、索引预热
十三、技术架构总结
四、步骤1:获取数据库环境信息
1. 获取 PostgreSQL 版本。
输入 SQL:
SELECT version();
输出结果(1行,耗时2.1ms):
| version |
| PostgreSQL 15.14 on x86_64-pc-linux-gnu, compiled by gcc (GCC) |
2. 获取关键数据库参数。
输入 SQL:
SELECT name, setting, unitFROM pg_settingsWHERE name IN ( 'shared_buffers', 'effective_cache_size', 'work_mem', 'maintenance_work_mem', 'max_connections', 'max_parallel_workers', 'max_parallel_workers_per_gather', 'wal_level')ORDER BY name;
输出结果(8行,耗时3.6ms):
| name | setting | unit |
| effective_cache_size | 524288 | 8KB |
| maintenance_work_mem | 524288 | KB |
| max_connections | 2048 | – |
| max_parallel_workers | 8 | – |
| max_parallel_workers_per_gather | 2 | – |
| shared_buffers | 1572864 | 8KB |
| wal_level | logical | – |
| work_mem | 4096 | KB |
注意:说明:
注意:shared_buffers = 12GB,effective_cache_size = 4GB,maintenance_work_mem = 512MB,满足向量检索场景要求。
五、步骤2:安装 pgvector 扩展
输入 SQL:
CREATE EXTENSION IF NOT EXISTS vector;
输出:执行成功,耗时1.7ms。
验证 SQL:
SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';
输出结果(1行,耗时1.9ms):
| extname | extversion |
| vector | 0.8.2 |
注意:说明:
注意:pgvector 0.8.2已安装,支持 HNSW 索引和 vector_cosine_ops 运算符。
七、步骤3:清理旧表(如果存在)
按依赖顺序删除,确保幂等执行:
DROP TABLE IF EXISTS chat_messages CASCADE; — 耗时5.7msDROP TABLE IF EXISTS chat_sessions CASCADE; — 耗时2.7msDROP TABLE IF EXISTS text_vectors CASCADE; — 耗时3.7msDROP TABLE IF EXISTS image_vectors CASCADE; — 耗时3.4ms
注意:说明:
注意:脚本支持幂等执行,重复运行不会报错。
九、步骤4:创建图片向量表 image_vectors(768维)
输入 SQL:
CREATE TABLE image_vectors ( id BIGSERIAL PRIMARY KEY, image_id VARCHAR(64) NOT NULL UNIQUE, image_url TEXT NOT NULL, thumbnail_url TEXT, embedding vector(768) NOT NULL, — 使用大模型生成 输出 768 维 category VARCHAR(100), tags TEXT[], source VARCHAR(50) DEFAULT 'upload', metadata JSONB DEFAULT '{}', status SMALLINT DEFAULT 1, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());COMMENT ON TABLE image_vectors IS '图片向量表 – 智能客服图片检索';COMMENT ON COLUMN image_vectors.embedding IS '使用大模型生成的768维图片向量';
输出:执行成功,耗时5.3ms + 1.8ms。
验证表结构:
SELECT column_name, data_type, is_nullable, column_defaultFROM information_schema.columnsWHERE table_name = 'image_vectors'ORDER BY ordinal_position;
输出结果(12行,耗时7.6ms):
| column_name | data_type | is_nullable | column_default |
| id | bigint | NO | nextval('image_vectors_id_seq'::regclass) |
| image_id | character varying | NO | – |
| image_url | text | NO | – |
| thumbnail_url | text | YES | – |
| embedding | USER-DEFINED | NO | – |
| category | character varying | YES | – |
| tags | ARRAY | YES | – |
| source | character varying | YES | 'upload'::character varying |
| metadata | jsonb | YES | '{}'::jsonb |
| status | smallint | YES | 1 |
| created_at | timestamp with time zone | YES | now() |
| updated_at | timestamp with time zone | YES | now() |
十、步骤5:创建文本向量表 text_vectors(1024维)
输入 SQL:
CREATE TABLE text_vectors ( id BIGSERIAL PRIMARY KEY, doc_id VARCHAR(64) NOT NULL UNIQUE, title TEXT, content TEXT NOT NULL, content_type VARCHAR(20) DEFAULT 'faq', embedding vector(1024) NOT NULL, — 大模型 输出 1024 维 category VARCHAR(100), tags TEXT[], source VARCHAR(100), metadata JSONB DEFAULT '{}', priority SMALLINT DEFAULT 0, status SMALLINT DEFAULT 1, expire_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());COMMENT ON TABLE text_vectors IS '文本向量表 – 智能客服知识库检索';COMMENT ON COLUMN text_vectors.embedding IS '大模型生成的1024维文本向量';
输出:执行成功,耗时4.3ms + 1.7ms。
验证表结构(15列):
| column_name | data_type | is_nullable | column_default |
| id | bigint | NO | nextval('text_vectors_id_seq'::regclass) |
| doc_id | character varying | NO | – |
| title | text | YES | – |
| content | text | NO | – |
| content_type | character varying | YES | 'faq'::character varying |
| embedding | USER-DEFINED | NO | – |
| category | character varying | YES | – |
| tags | ARRAY | YES | – |
| source | character varying | YES | – |
| metadata | jsonb | YES | '{}'::jsonb |
| priority | smallint | YES | 0 |
| status | smallint | YES | 1 |
| expire_at | timestamp with time zone | YES | – |
| created_at | timestamp with time zone | YES | now() |
| updated_at | timestamp with time zone | YES | now() |
十一、步骤6:创建对话记录表 chat_sessions + chat_messages
输入 SQL(会话表):
CREATE TABLE chat_sessions ( id BIGSERIAL PRIMARY KEY, session_id VARCHAR(64) NOT NULL UNIQUE, user_id VARCHAR(64) NOT NULL, channel VARCHAR(20) DEFAULT 'web', status VARCHAR(20) DEFAULT 'active', created_at TIMESTAMPTZ DEFAULT NOW(), closed_at TIMESTAMPTZ);
输出:执行成功,耗时3.5ms。
输入 SQL(消息表):
CREATE TABLE chat_messages ( id BIGSERIAL PRIMARY KEY, session_id VARCHAR(64) NOT NULL, role VARCHAR(10) NOT NULL, content_type VARCHAR(20) DEFAULT 'text', content TEXT NOT NULL, image_url TEXT, embedding vector(1024), — 1024维向量用于相似问题推荐 matched_doc_ids TEXT[], confidence REAL, feedback SMALLINT, created_at TIMESTAMPTZ DEFAULT NOW());COMMENT ON TABLE chat_messages IS '对话消息表 – 含向量字段用于相似问题推荐';
输出:执行成功,耗时3.5ms + 1.7ms。
确认所有核心表:
SELECT table_name, pg_size_pretty(pg_total_relation_size(quote_ident(table_name)))FROM information_schema.tablesWHERE table_schema = 'public' AND table_name IN ('image_vectors', 'text_vectors', 'chat_sessions', 'chat_messages')ORDER BY table_name;
输出结果(4行,耗时3.3ms):
| table_name | pg_size_pretty |
| chat_messages | 16KB |
| chat_sessions | 16KB |
| image_vectors | 24KB |
| text_vectors | 24KB |
十三、步骤7:创建 B-Tree + GIN 辅助索引
共创建11个辅助索引:
| # | 索引名 | 类型 | 说明 | 耗时 |
| 1 | idx_image_vectors_category | B-Tree(部分) | 图片分类,WHERE status=1 | 2.2ms |
| 2 | idx_image_vectors_status | B-Tree | 图片状态 | 2.1ms |
| 3 | idx_text_vectors_category | B-Tree(部分) | 文本分类,WHERE status=1 | 2.1ms |
| 4 | idx_text_vectors_content_type | B-Tree(部分) | 文本类型,WHERE status=1 | 2.1ms |
| 5 | idx_text_vectors_priority | B-Tree(部分) | 文本优先级降序,WHERE status=1 | 2.1ms |
| 6 | idx_chat_messages_session | B-Tree | 消息-会话复合索引 | 2.2ms |
| 7 | idx_chat_sessions_user | B-Tree | 会话-用户索引(时间降序) | 2.1ms |
| 8 | idx_image_vectors_tags | GIN(部分) | 图片标签数组,WHERE status=1 | 2.2ms |
| 9 | idx_text_vectors_tags | GIN(部分) | 文本标签数组,WHERE status=1 | 2.0ms |
| 10 | idx_image_vectors_metadata | GIN | 图片 JSONB 元数据 | 2.0ms |
| 11 | idx_text_vectors_metadata | GIN | 文本 JSONB 元数据 | 2.0ms |
各索引创建 SQL 示例(部分索引):
— 图片分类索引(部分索引,仅 status=1 的记录)CREATE INDEX idx_image_vectors_category ON image_vectors (category) WHERE status = 1;– 图片标签 GIN 索引CREATE INDEX idx_image_vectors_tags ON image_vectors USING GIN (tags) WHERE status = 1;– 图片元数据 JSONB GIN 索引CREATE INDEX idx_image_vectors_metadata ON image_vectors USING GIN (metadata);
列出所有已创建索引:
SELECT indexname, indexdefFROM pg_indexesWHERE schemaname = 'public' AND tablename IN ('image_vectors', 'text_vectors', 'chat_sessions', 'chat_messages')ORDER BY tablename, indexname;
输出结果(18行,耗时3.4ms):
| indexname | 表 |
| chat_messages_pkey | chat_messages |
| idx_chat_messages_session | chat_messages |
| chat_sessions_pkey | chat_sessions |
| chat_sessions_session_id_key | chat_sessions |
| idx_chat_sessions_user | chat_sessions |
| idx_image_vectors_category | image_vectors |
| idx_image_vectors_metadata | image_vectors |
| idx_image_vectors_status | image_vectors |
| idx_image_vectors_tags | image_vectors |
| image_vectors_image_id_key | image_vectors |
| image_vectors_pkey | image_vectors |
| idx_text_vectors_category | text_vectors |
| idx_text_vectors_content_type | text_vectors |
| idx_text_vectors_metadata | text_vectors |
| idx_text_vectors_priority | text_vectors |
| idx_text_vectors_tags | text_vectors |
| text_vectors_doc_id_key | text_vectors |
| text_vectors_pkey | text_vectors |
十五、步骤8:插入 FAQ 知识库数据(12条)
注意:说明:
注意:embedding 字段需要你使用大模型生成。
输入 SQL 示例(faq-001):
INSERT INTO text_vectors (doc_id, title, content, content_type, embedding, category, tags, priority)VALUES ( 'faq-001', '如何重置密码', '你可以通过以下步骤重置密码:1. 单击登录页面的"忘记密码"链接;2. 输入注册邮箱; 3. 查收重置邮件;4. 单击邮件中的链接设置新密码。', 'faq', (SELECT array_agg(random())::vector(1024) FROM generate_series(1, 1024)), '账号管理', ARRAY['账号管理', 'faq'], 2);
共插入12条 FAQ 数据,全部成功:
| doc_id | 标题 | 分类 | 优先级 | 耗时 |
| faq-001 | 如何重置密码 | 账号管理 | 2 | 2.6ms |
| faq-002 | 如何修改个人信息 | 账号管理 | 1 | 2.0ms |
| faq-003 | 订单发货时间 | 订单物流 | 2 | 1.9ms |
| faq-004 | 退货退款流程 | 售后服务 | 2 | 1.9ms |
| faq-005 | 如何使用优惠券 | 优惠活动 | 1 | 1.9ms |
| faq-006 | 配送范围和运费 | 订单物流 | 1 | 2.1ms |
| faq-007 | 如何联系人工客服 | 客服帮助 | 2 | 2.0ms |
| faq-008 | 账号安全设置 | 账号管理 | 1 | 2.0ms |
| faq-009 | 支付方式说明 | 支付相关 | 1 | 2.0ms |
| faq-010 | 会员等级权益 | 会员服务 | 1 | 2.0ms |
| faq-011 | 商品保修政策 | 售后服务 | 1 | 2.1ms |
| faq-012 | 发票开具说明 | 支付相关 | 0 | 2.0ms |
验证 SQL:
SELECT doc_id, title, category, priority, status, LEFT(content, 40) || '…' AS content_previewFROM text_vectorsORDER BY priority DESC, doc_id;
输出结果(12行,耗时1.9ms):
| doc_id | title | category | priority | status | content_preview |
| faq-001 | 如何重置密码 | 账号管理 | 2 | 1 | 你可以通过以下步骤重置密码:1. 单击登录页面的"忘记密码"链接;2. … |
| faq-003 | 订单发货时间 | 订单物流 | 2 | 1 | 普通订单在付款后48小时内发货,加急订单在24小时内发货。节假日期间发货时间可能… |
| faq-004 | 退货退款流程 | 售后服务 | 2 | 1 | 申请退货流程:1. 进入"我的订单"找到对应订单;2. 单击"申请退货";3. … |
| faq-007 | 如何联系人工客服 | 客服帮助 | 2 | 1 | 你可以通过以下方式联系人工客服:1. APP内单击"在线客服"转人工;2. 拨打… |
| faq-002 | 如何修改个人信息 | 账号管理 | 1 | 1 | 登录后进入"个人中心",单击"编辑资料"按钮,可修改昵称、头像、手机号等个人信息… |
| faq-005 | 如何使用优惠券 | 优惠活动 | 1 | 1 | 在购物车页面或结算页面,单击"使用优惠券",选择可用的优惠券即可抵扣。注意:优惠… |
| faq-006 | 配送范围和运费 | 订单物流 | 1 | 1 | 我们支持全国配送(港澳台及海外地区除外)。订单满99元免运费,未满99元收取8元… |
| faq-008 | 账号安全设置 | 账号管理 | 1 | 1 | 建议你开启以下安全设置:1. 设置复杂密码(包含字母、数字和特殊字符);2. … |
| faq-009 | 支付方式说明 | 支付相关 | 1 | 1 | 我们支持以下支付方式:微信支付、支付宝、银行卡快捷支付、信用卡支付、花呗分期。… |
| faq-010 | 会员等级权益 | 会员服务 | 1 | 1 | 会员分为普通、银卡、金卡、钻石四个等级。升级条件基于年消费金额,享受不同折扣… |
| faq-011 | 商品保修政策 | 售后服务 | 1 | 1 | 电子产品享受1年质保,服装类商品7天无理由退换,食品类商品保质期内可退换。… |
| faq-012 | 发票开具说明 | 支付相关 | 0 | 1 | 订单完成后可在"我的订单"中申请电子发票,支持增值税普通发票和增值税专用发票。… |
十六、步骤9:插入图片向量示例数据(8条)
注意:说明:
注意:embedding 字段需要你使用大模型生成。
输入 SQL 示例(img-001):
INSERT INTO image_vectors (image_id, image_url, embedding, category, tags, metadata)VALUES ( 'img-001', 'https://cdn.example.com/products/phone_x1.jpg', (SELECT array_agg(random())::vector(768) FROM generate_series(1, 768)), '产品图片', ARRAY['产品图片', 'smart_cs'], '{"width":800,"height":600,"format":"jpg"}'::jsonb);
共插入8条图片数据,全部成功:
| image_id | 分类 | URL | 耗时 |
| img-001 | 产品图片 | …/products/phone_x1.jpg | 2.3ms |
| img-002 | 产品图片 | …/products/laptop_pro.jpg | 2.0ms |
| img-003 | 产品图片 | …/products/headphone_bt.jpg | 2.0ms |
| img-004 | 故障图片 | …/faults/screen_crack.jpg | 1.9ms |
| img-005 | 故障图片 | …/faults/battery_swell.jpg | 1.9ms |
| img-006 | 说明书 | …/manuals/phone_guide.jpg | 1.9ms |
| img-007 | 产品图片 | …/products/tablet_air.jpg | 1.9ms |
| img-008 | 产品图片 | …/products/watch_smart.jpg | 1.9ms |
十七、步骤10:插入对话示例数据
会话插入 SQL:
INSERT INTO chat_sessions (session_id, user_id, channel, status)VALUES ('sess-001', 'user-1001', 'web', 'active'), ('sess-002', 'user-1002', 'wechat', 'active'), ('sess-003', 'user-1003', 'app', 'closed');
输出:影响3行,耗时1.9ms
共插入8条对话消息(3个会话):
| session_id | role | content_type | 内容预览 | 耗时 |
| sess-001 | user | text | 我的密码忘了怎么办? | 2.1ms |
| sess-001 | assistant | text | 你可以通过"忘记密码"链接重置密码… | 2.0ms |
| sess-001 | user | text | 重置密码后多久生效? | 1.9ms |
| sess-002 | user | text | 我的订单什么时候发货? | 2.2ms |
| sess-002 | assistant | text | 普通订单在付款后48小时内发货… | 2.1ms |
| sess-002 | user | image | 这个商品有质量问题 | 2.0ms |
| sess-003 | user | text | 怎么申请退款? | 1.9ms |
| sess-003 | assistant | text | 退货退款流程:进入我的订单… | 2.0ms |
十九、步骤11:执行 ANALYZE 更新统计信息
ANALYZE image_vectors; — 耗时2.8msANALYZE text_vectors; — 耗时2.1msANALYZE chat_sessions; — 耗时1.8msANALYZE chat_messages; — 耗时1.9ms
各表数据量统计:
SELECT 'image_vectors' AS table_name, count(*) AS row_count FROM image_vectorsUNION ALL SELECT 'text_vectors', count(*) FROM text_vectorsUNION ALL SELECT 'chat_sessions', count(*) FROM chat_sessionsUNION ALL SELECT 'chat_messages', count(*) FROM chat_messagesORDER BY table_name;
输出结果(4行,耗时2.5ms):
| table_name | row_count |
| chat_messages | 8 |
| chat_sessions | 3 |
| image_vectors | 8 |
| text_vectors | 12 |
注意:说明:
注意:4张核心表,31条示例数据。
21、步骤12:图片向量 HNSW 索引(768维)
输入 SQL:
CREATE INDEX idx_image_vectors_embedding_cosineON image_vectorsUSING hnsw (embedding vector_cosine_ops)WITH (m = 16, ef_construction = 128);
输出:执行成功,耗时2.7ms。
22、步骤13:文本向量 HNSW 索引(1024维)
输入 SQL:
CREATE INDEX idx_text_vectors_embedding_cosineON text_vectorsUSING hnsw (embedding vector_cosine_ops)WITH (m = 16, ef_construction = 128);
输出:执行成功,耗时2.6ms。
23、步骤14:对话消息向量 HNSW 索引(1024维)
输入 SQL:
CREATE INDEX idx_chat_messages_embedding_cosineON chat_messagesUSING hnsw (embedding vector_cosine_ops)WITH (m = 16, ef_construction = 64);
输出:执行成功,耗时2.5ms。
查看 HNSW 索引大小:
SELECT indexname AS index_name, pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_size, indexdefFROM pg_indexesWHERE schemaname = 'public' AND indexname LIKE '%embedding_cosine'ORDER BY indexname;
输出结果(3行,耗时2.9ms):
| index_name | index_size | 说明 |
| idx_chat_messages_embedding_cosine | 72KB | 消息表1024维 |
| idx_image_vectors_embedding_cosine | 40KB | 图片表768维 |
| idx_text_vectors_embedding_cosine | 104KB | 文本表1024维 |
注意:说明:
注意:HNSW 索引参数:
25、步骤15:设置 HNSW 搜索参数
输入 SQL:
— 设置搜索候选列表大小(值越大检索越精确,推荐100 ~ 200)SET hnsw.ef_search = 100;– 开启迭代扫描模式(解决 WHERE 过滤时结果不足的问题)SET hnsw.iterative_scan = relaxed_order;
输出:两条均执行成功。
确认设置:
SHOW hnsw.ef_search;
输出结果:
| hnsw.ef_search |
| 100 |
建议:生产环境可将参数写入 postgresql.conf:
# pgvector HNSW 搜索参数hnsw.ef_search = 100hnsw.iterative_scan = relaxed_order
27、步骤16:以图搜图(余弦相似度 Top 5)
输入 SQL:
SELECT iv.image_id, iv.image_url, iv.category, iv.tags, 1 – (iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-001')) AS similarityFROM image_vectors ivWHERE iv.status = 1ORDER BY iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-001')LIMIT 5;
输出结果(5行,耗时2.6ms):
| image_id | category | similarity | 说明 |
| img-001 | 产品图片 | 1.0000 | 查询向量本身 |
| img-008 | 产品图片 | 0.7696 | 智能手表 |
| img-006 | 说明书 | 0.7631 | 手机说明书 |
| img-007 | 产品图片 | 0.7535 | 平板电脑 |
| img-003 | 产品图片 | 0.7471 | 蓝牙耳机 |
注意:说明:
注意:验证通过,自身相似度为1.0,其余按余弦相似度降序排列。
28、步骤17:分类过滤图片检索(category='产品图片')
输入 SQL:
SELECT iv.image_id, iv.image_url, iv.category, 1 – (iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-004')) AS similarityFROM image_vectors ivWHERE iv.status = 1 AND iv.category = '产品图片'ORDER BY iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-004')LIMIT 5;
输出结果(5行,耗时1.9ms):
| image_id | category | similarity |
| img-001 | 产品图片 | 0.7459 |
| img-008 | 产品图片 | 0.7355 |
| img-002 | 产品图片 | 0.7300 |
| img-007 | 产品图片 | 0.7265 |
| img-003 | 产品图片 | 0.7218 |
注意:说明:
注意:验证通过,以故障图片 img-004 为查询向量,仅在"产品图片"分类下检索,结果符合预期。
29、步骤18:FAQ 语义检索(余弦相似度 Top 5)
输入 SQL:
SELECT tv.doc_id, tv.title, tv.category, tv.priority, 1 – (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-001')) AS similarityFROM text_vectors tvWHERE tv.status = 1 AND (tv.expire_at IS NULL OR tv.expire_at > NOW())ORDER BY tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-001')LIMIT 5;
输出结果(5行,耗时2.1ms):
| doc_id | title | category | priority | similarity |
| faq-001 | 如何重置密码 | 账号管理 | 2 | 1.0000 |
| faq-003 | 订单发货时间 | 订单物流 | 2 | 0.7757 |
| faq-006 | 配送范围和运费 | 订单物流 | 1 | 0.7662 |
| faq-004 | 退货退款流程 | 售后服务 | 2 | 0.7635 |
| faq-009 | 支付方式说明 | 支付相关 | 1 | 0.7580 |
注意:说明:
注意:验证通过,FAQ 语义检索正常工作,支持过期时间过滤。
30、步骤19:带置信度阈值的检索(similarity > 0.5)
输入 SQL:
SELECT tv.doc_id, tv.title, tv.category, 1 – (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-004')) AS similarityFROM text_vectors tvWHERE tv.status = 1 AND 1 – (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-004')) > 0.5ORDER BY tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-004')LIMIT 5;
输出结果(5行,耗时2.3ms):
| doc_id | title | category | similarity |
| faq-004 | 退货退款流程 | 售后服务 | 1.0000 |
| faq-001 | 如何重置密码 | 账号管理 | 0.7635 |
| faq-008 | 账号安全设置 | 账号管理 | 0.7594 |
| faq-006 | 配送范围和运费 | 订单物流 | 0.7557 |
| faq-003 | 订单发货时间 | 订单物流 | 0.7528 |
注意:说明:
31、步骤20:混合检索(向量70% + 全文30%)
输入 SQL:
SELECT tv.doc_id, tv.title, LEFT(tv.content, 50) AS content_preview, 1 – (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-003')) AS vector_score, ts_rank(to_tsvector('simple', tv.content), plainto_tsquery('simple', '发货')) AS text_score, 0.7 * (1 – (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-003'))) + 0.3 * ts_rank(to_tsvector('simple', tv.content), plainto_tsquery('simple', '发货')) AS hybrid_scoreFROM text_vectors tvWHERE tv.status = 1ORDER BY hybrid_score DESCLIMIT 5;
输出结果(5行,耗时2.5ms):
| doc_id | title | vector_score | text_score | hybrid_score |
| faq-003 | 订单发货时间 | 1.0000 | 0.0 | 0.7000 |
| faq-001 | 如何重置密码 | 0.7757 | 0.0 | 0.5430 |
| faq-011 | 商品保修政策 | 0.7712 | 0.0 | 0.5398 |
| faq-009 | 支付方式说明 | 0.7677 | 0.0 | 0.5374 |
| faq-007 | 如何联系人工客服 | 0.7652 | 0.0 | 0.5356 |
注意:说明:
注意:示例数据使用随机向量,全文检索中关键词"发货"只在 faq-003 标题匹配。生产环境中真实 Embedding 会使 hybrid_score 更有区分度。
32、步骤21:相似问题推荐(对话消息向量检索)
输入 SQL:
SELECT cm.session_id, cm.role, LEFT(cm.content, 50) AS content_preview, 1 – (cm.embedding <=> (SELECT embedding FROM chat_messages WHERE content LIKE '%密码%' LIMIT 1)) AS similarityFROM chat_messages cmWHERE cm.role = 'user'ORDER BY cm.embedding <=> (SELECT embedding FROM chat_messages WHERE content LIKE '%密码%' LIMIT 1)LIMIT 5;
输出结果(5行,耗时2.0ms):
| session_id | role | content_preview | similarity |
| sess-001 | user | 我的密码忘了怎么办? | 1.0000 |
| sess-002 | user | 这个商品有质量问题 | 0.7460 |
| sess-001 | user | 重置密码后多久生效? | 0.7460 |
| sess-002 | user | 我的订单什么时候发货? | 0.7454 |
| sess-003 | user | 怎么申请退款? | 0.7338 |
注意:说明:
注意:验证通过,基于用户历史提问的向量检索,可用于"猜你想问"功能。
34、步骤22:EXPLAIN ANALYZE 验证查询计划
输入 SQL:
EXPLAIN (ANALYZE, BUFFERS)SELECT iv.image_id, 1 – (iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-001')) AS similarityFROM image_vectors ivWHERE iv.status = 1ORDER BY iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-001')LIMIT 10;
输出结果(耗时2.0ms):
Limit (cost=3.48..3.50 rows=8 width=24) (actual time=0.097…) Buffers: shared hit=67 InitPlan 1 (returns $0) -> Seq Scan on image_vectors (cost=0.00..1.10 rows=1 …) Filter: ((image_id)::text = 'img-001'::text) Rows Removed by Filter: 7 Buffers: shared hit=1 InitPlan 2 (returns $1) -> Seq Scan on image_vectors image_vectors_1 (cost=0.00..1.10 …) Filter: ((image_id)::text = 'img-001'::text) Rows Removed by Filter: 7 Buffers: shared hit=1 -> Sort (cost=1.28..1.30 rows=8 width=24) (actual time=0….) Sort Key: ((iv.embedding <=> $1)) Sort Method: quicksort Memory: 25kB Buffers: shared hit=67 -> Seq Scan on image_vectors iv (cost=0.00..1.16 …) Filter: (status = 1) Buffers: shared hit=67Planning Time: 0.101 msExecution Time: 0.115 ms
分析:当前仅8行数据,PostgreSQL 优化器正确选择了 Seq Scan(对小表更高效)。当数据量增长到数万行以上时,HNSW Index Scan 将自动启用。之前的50万行验证中已确认 HNSW 索引命中,查询延迟仅1.753ms。
36、步骤23:向量表存储统计
输入 SQL:
SELECT c.relname AS table_name, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size, pg_size_pretty(pg_relation_size(c.oid)) AS data_size, pg_size_pretty(pg_indexes_size(c.oid)) AS index_size, s.n_live_tup AS row_countFROM pg_stat_user_tables sJOIN pg_class c ON s.relid = c.oidWHERE c.relname IN ('image_vectors', 'text_vectors', 'chat_sessions', 'chat_messages')ORDER BY pg_total_relation_size(c.oid) DESC;
输出结果(4行,耗时3.9ms):
| table_name | total_size | data_size | index_size | row_count |
| text_vectors | 344 kB | 8192 bytes | 232 kB | 12 |
| image_vectors | 232 kB | 8192 bytes | 152 kB | 8 |
| chat_messages | 200 kB | 8192 bytes | 104 kB | 8 |
| chat_sessions | 56 kB | 8192 bytes | 48 kB | 3 |
37、步骤24:索引使用情况
输入 SQL:
SELECT indexrelname AS index_name, idx_scan AS times_used, pg_size_pretty(pg_relation_size(indexrelid)) AS index_sizeFROM pg_stat_user_indexesWHERE schemaname = 'public'ORDER BY idx_scan DESC;
输出结果(21个索引,耗时2.8ms):
| index_name | times_used | index_size |
| image_vectors_pkey | 0 | 16KB |
| image_vectors_image_id_key | 0 | 16KB |
| text_vectors_pkey | 0 | 16KB |
| text_vectors_doc_id_key | 0 | 16KB |
| chat_sessions_pkey | 0 | 16KB |
| chat_sessions_session_id_key | 0 | 16KB |
| chat_messages_pkey | 0 | 16KB |
| idx_image_vectors_category | 0 | 16KB |
| idx_image_vectors_status | 0 | 16KB |
| idx_text_vectors_category | 0 | 16KB |
| idx_text_vectors_content_type | 0 | 16KB |
| idx_text_vectors_priority | 0 | 16KB |
| idx_chat_messages_session | 0 | 16KB |
| idx_chat_sessions_user | 0 | 16KB |
| idx_image_vectors_tags | 0 | 24KB |
| idx_text_vectors_tags | 0 | 24KB |
| idx_image_vectors_metadata | 0 | 24KB |
| idx_text_vectors_metadata | 0 | 24KB |
| idx_image_vectors_embedding_cosine | 0 | 40KB |
| idx_text_vectors_embedding_cosine | 0 | 104KB |
| idx_chat_messages_embedding_cosine | 0 | 72KB |
注意:说明:
注意:刚部署完成,索引使用次数均为0。生产环境运行后此监控可用于发现未使用的索引。
38、步骤25:缓冲区命中率
输入 SQL:
SELECT ROUND( sum(heap_blks_hit)::numeric / NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100, 2 ) AS buffer_hit_ratio_pctFROM pg_statio_user_tables;
输出结果(1行,耗时2.7ms):
| buffer_hit_ratio_pct |
| – (新表暂无统计) |
注意:说明:
注意:刚创建的表尚无缓冲区读取统计。建议生产环境中此值应保持在 > 95%,低于 90% 需要增加 shared_buffers。
40、步骤26:预热 HNSW 索引到共享缓冲区
输入 SQL:
— 安装 pg_prewarm 扩展CREATE EXTENSION IF NOT EXISTS pg_prewarm;– 预热图片向量 HNSW 索引SELECT pg_prewarm('idx_image_vectors_embedding_cosine') AS pages_prewarmed;– 预热文本向量 HNSW 索引SELECT pg_prewarm('idx_text_vectors_embedding_cosine') AS pages_prewarmed;
输出结果:
| 操作 | pages_prewarmed | 耗时 |
| 安装 pg_prewarm | – | 1.7ms |
| 预热图片向量索引 | 5页 | 2.0ms |
| 预热文本向量索引 | 13页 | 1.7ms |
注意:说明:
注意:pg_prewarm 将索引页面加载到 shared_buffers,减少首次查询的磁盘 I/O。建议在数据库重启后或大量数据导入后执行。
41、十三、技术架构总结
┌─────────────────────────────────────────────────────┐│ 智能客服应用层 │├──────────┬──────────┬──────────┬────────────────────┤│ 以图搜图 │ FAQ检索 │ 混合检索 │ 相似问题推荐 │├──────────┴──────────┴──────────┴────────────────────┤│ pgvector HNSW 索引层 ││ • image_vectors: 768维 (CLIP ViT-L/14) ││ • text_vectors: 1024维 (BGE-large-zh-v1.5) ││ • chat_messages: 1024维 (对话向量) │├─────────────────────────────────────────────────────┤│ PostgreSQL 15.14 + pgvector 0.8.2 ││ • 余弦距离: <=> 运算符 ││ • HNSW 参数: m=16, ef_search=100 ││ • 辅助索引: B-Tree + GIN (部分索引) │└─────────────────────────────────────────────────────┘