纲要
本文档梳理了基于 PostgreSQL 与 pgvector 扩展构建实时推荐与搜索系统的核心概念、技术流程与代码实现。
- 核心概念
- 实时推荐与搜索
- 向量相似性检索
- 全文检索
- 混合检索
- 个性化推荐
- 技术栈与组件
PostgreSQL(数据库核心)pgvector(向量存储与检索扩展)tsvector/tsquery(全文检索)GIN索引 (全文检索加速)HNSW/IVFFlat索引 (向量检索加速)- 距离操作符:
<->(L2距离),<=>(余弦距离) - 排序与评分函数:
ts_rank(), 自定义热度与时序衰减函数
- 系统架构流程
- 数据层: 商品表, 用户画像表, 用户行为表
- 计算层: SQL 动态查询, 向量计算, 业务过滤, 排序逻辑
- 加速层: 多类型索引, 可选的缓存层与连接池 (
PgBouncer)
- 核心查询场景
- 业务过滤 + 向量
Top-K检索 - 全文检索 + 语义检索混合排序
- 引入热度与时效性的复合排序
- 基于用户画像的个性化推荐
- 业务过滤 + 向量
实时推荐与搜索概述
在现代电商、内容平台和资讯应用中,实时推荐与搜索已成为核心用户体验组件。其关键在于系统能够在极短时间内(毫秒级),基于用户当前的输入、历史行为和实时上下文,动态计算并返回最相关的结果。
传统实现通常依赖复杂的组件栈,如 Elasticsearch 用于全文检索,Faiss 用于向量检索,再辅以独立的特征服务和排序服务。这种架构带来了显著的运维复杂度和性能开销。
PostgreSQL 结合 pgvector 扩展提供了一种极具吸引力的替代方案。它将全文检索、向量语义检索、业务数据过滤和复杂排序能力整合在同一个数据库中,极大地简化了技术栈,降低了数据一致性和运维的复杂性。
系统核心流程
一个典型的实时推荐或搜索请求,其核心处理流程可在数据库单次查询中完成:
#mermaid-svg-N2HLf3UUyyRvRQ0K{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N2HLf3UUyyRvRQ0K .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N2HLf3UUyyRvRQ0K .error-icon{fill:#552222;}#mermaid-svg-N2HLf3UUyyRvRQ0K .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N2HLf3UUyyRvRQ0K .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N2HLf3UUyyRvRQ0K .marker.cross{stroke:#333333;}#mermaid-svg-N2HLf3UUyyRvRQ0K svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N2HLf3UUyyRvRQ0K p{margin:0;}#mermaid-svg-N2HLf3UUyyRvRQ0K .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N2HLf3UUyyRvRQ0K .cluster-label text{fill:#333;}#mermaid-svg-N2HLf3UUyyRvRQ0K .cluster-label span{color:#333;}#mermaid-svg-N2HLf3UUyyRvRQ0K .cluster-label span p{background-color:transparent;}#mermaid-svg-N2HLf3UUyyRvRQ0K .label text,#mermaid-svg-N2HLf3UUyyRvRQ0K span{fill:#333;color:#333;}#mermaid-svg-N2HLf3UUyyRvRQ0K .node rect,#mermaid-svg-N2HLf3UUyyRvRQ0K .node circle,#mermaid-svg-N2HLf3UUyyRvRQ0K .node ellipse,#mermaid-svg-N2HLf3UUyyRvRQ0K .node polygon,#mermaid-svg-N2HLf3UUyyRvRQ0K .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N2HLf3UUyyRvRQ0K .rough-node .label text,#mermaid-svg-N2HLf3UUyyRvRQ0K .node .label text,#mermaid-svg-N2HLf3UUyyRvRQ0K .image-shape .label,#mermaid-svg-N2HLf3UUyyRvRQ0K .icon-shape .label{text-anchor:middle;}#mermaid-svg-N2HLf3UUyyRvRQ0K .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N2HLf3UUyyRvRQ0K .rough-node .label,#mermaid-svg-N2HLf3UUyyRvRQ0K .node .label,#mermaid-svg-N2HLf3UUyyRvRQ0K .image-shape .label,#mermaid-svg-N2HLf3UUyyRvRQ0K .icon-shape .label{text-align:center;}#mermaid-svg-N2HLf3UUyyRvRQ0K .node.clickable{cursor:pointer;}#mermaid-svg-N2HLf3UUyyRvRQ0K .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N2HLf3UUyyRvRQ0K .arrowheadPath{fill:#333333;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N2HLf3UUyyRvRQ0K .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N2HLf3UUyyRvRQ0K .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N2HLf3UUyyRvRQ0K .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N2HLf3UUyyRvRQ0K .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N2HLf3UUyyRvRQ0K .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N2HLf3UUyyRvRQ0K .cluster text{fill:#333;}#mermaid-svg-N2HLf3UUyyRvRQ0K .cluster span{color:#333;}#mermaid-svg-N2HLf3UUyyRvRQ0K div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N2HLf3UUyyRvRQ0K .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N2HLf3UUyyRvRQ0K rect.text{fill:none;stroke-width:0;}#mermaid-svg-N2HLf3UUyyRvRQ0K .icon-shape,#mermaid-svg-N2HLf3UUyyRvRQ0K .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N2HLf3UUyyRvRQ0K .icon-shape p,#mermaid-svg-N2HLf3UUyyRvRQ0K .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N2HLf3UUyyRvRQ0K .icon-shape .label rect,#mermaid-svg-N2HLf3UUyyRvRQ0K .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N2HLf3UUyyRvRQ0K .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N2HLf3UUyyRvRQ0K .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N2HLf3UUyyRvRQ0K :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据存储
用户发起请求
应用层构建SQL
PostgreSQL 执行引擎
业务过滤 WHERE
向量相似度检索 ORDER BY dist
全文检索匹配 tsvector AND tsquery
结果排序与打分 ts_rank 自定义评分
返回Top-K结果
商品表 Products
用户画像 Profiles
用户行为 Events
环境准备与数据模型
为演示实时推荐与搜索,我们构建一个电商商品库示例。该示例包含数万条商品数据,每条记录包含基本属性、文本描述以及对应的文本嵌入向量。
启用扩展
首先,确保数据库已安装并启用 pgvector 扩展:
sql
CREATE EXTENSION IF NOT EXISTS vector;
核心表结构设计
数据模型主要围绕三张表构建:products (商品表), user_profiles (用户画像表), user_events (用户行为表)。
sql
-- 商品表
CREATE TABLE products (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
category VARCHAR(100),
price NUMERIC(10, 2),
description TEXT,
-- 存储文本嵌入向量,维度为8,实际场景如384, 768, 1536等
embedding VECTOR(8),
-- 使用生成列自动维护全文检索向量,避免手动更新
tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', title || ' ' || COALESCE(description, ''))) STORED
);
-- 用户画像表
CREATE TABLE user_profiles (
user_id VARCHAR(50) PRIMARY KEY,
preference_vector VECTOR(8),
user_segment VARCHAR(20) -- 如:'高端用户', '普通用户'
);
-- 用户行为表
CREATE TABLE user_events (
id SERIAL PRIMARY KEY,
user_id VARCHAR(50) REFERENCES user_profiles(user_id),
product_id INTEGER REFERENCES products(id),
event_type VARCHAR(20), -- 'view', 'click', 'purchase'
event_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
辅助函数与数据填充
为了演示,我们创建辅助函数生成随机向量,并插入示例数据。
sql
-- 创建生成随机向量的函数
CREATE OR REPLACE FUNCTION random_vector(dim INTEGER) RETURNS VECTOR AS $$
BEGIN
RETURN (SELECT array_agg(random() * 100)::VECTOR FROM generate_series(1, dim));
END;
$$ LANGUAGE plpgsql;
-- 插入商品示例数据 (5万条)
INSERT INTO products (title, category, price, description, embedding)
SELECT
'商品-' || generate_series,
(ARRAY['手机', '电脑', '家居', '相机', '耳机'])[floor(random() * 5 + 1)],
round((random() * 5000 + 100)::numeric, 2),
'这是描述信息-' || generate_series,
random_vector(8)
FROM generate_series(1, 50000);
-- 模拟特定品牌商品
INSERT INTO products (title, category, price, description, embedding)
VALUES
('Apple iPhone 15 Pro', '手机', 799.00, '高端智能手机', random_vector(8)),
('Samsung Galaxy S24 Ultra', '手机', 999.00, '安卓旗舰手机', random_vector(8)),
('Xiaomi 14 Pro', '手机', 599.00, '高性价比旗舰手机', random_vector(8));
-- 插入用户画像
INSERT INTO user_profiles (user_id, preference_vector, user_segment)
VALUES
('user_tech', random_vector(8), '高端用户'),
('user_budget', random_vector(8), '普通用户');
-- 插入用户行为
INSERT INTO user_events (user_id, product_id, event_type)
VALUES
('user_tech', 1, 'view'),
('user_tech', 2, 'click'),
('user_budget', 3, 'view');
索引创建
为保障查询性能,需为全文检索和向量检索分别创建索引。
sql
-- 为全文检索创建 GIN 索引
CREATE INDEX idx_products_tsv ON products USING GIN (tsv);
-- 为向量检索创建索引 (此处创建 IVFFlat 和 HNSW 两种以供对比)
-- IVFFlat 索引: 构建速度快,适合大数据集
CREATE INDEX idx_products_embedding_ivfflat ON products USING IVFFLAT (embedding vector_l2_ops) WITH (lists = 100);
-- HNSW 索引: 查询速度极快,但构建和内存消耗较大
-- 可根据实际情况选择其一或同时创建
CREATE INDEX idx_products_embedding_hnsw ON products USING HNSW (embedding vector_l2_ops);
核心查询场景详解
以下通过四个典型查询场景,展示 PostgreSQL + pgvector 在实时推荐与搜索中的强大能力。
场景一: 业务过滤与向量Top-K检索
需求 : 用户搜索"价格在500-1500元之间的高端手机",返回最匹配的10款商品。
逻辑: 先通过业务字段(分类、价格)进行过滤,再与目标向量计算相似度,按距离升序排列。
sql
-- 使用 L2 距离操作符 <->,按距离升序排列
SELECT
id,
title,
category,
price,
embedding <-> '[1, 2, 3, 4, 5, 6, 7, 8]'::VECTOR AS distance
FROM
products
WHERE
category = '手机'
AND price BETWEEN 500 AND 1500
ORDER BY
embedding <-> '[1, 2, 3, 4, 5, 6, 7, 8]'::VECTOR
LIMIT 10;
性能分析 :
默认情况下,优化器可能因代价估算选择全表扫描。通过 EXPLAIN 可查看执行计划。若需强制使用向量索引,可调整查询或通过 enable_seqscan 等参数进行调优(生产环境不推荐随意更改全局设置)。此场景为经典的 Top-K 查询,在毫秒级即可完成。
场景二: 全文检索与语义检索的混合排序
需求 : 搜索标题或描述中包含"iPhone"或"Camera"的商品,并结合向量语义相似度进行综合排序。
逻辑 : 使用 tsvector 进行关键词匹配,并使用 ts_rank 计算文本相关度。同时,计算向量相似度(距离越小,相似度越高),最后加权整合为最终得分 hybrid_score。
sql
WITH search_query AS (
SELECT
to_tsquery('simple', 'iphone | camera') AS query_vector,
'[1, 2, 3, 4, 5, 6, 7, 8]'::VECTOR AS target_embedding
)
SELECT
p.id,
p.title,
p.category,
p.price,
-- 计算最终混合得分:文本相关度 + 向量相似度
(COALESCE(ts_rank(p.tsv, sq.query_vector), 0) * 2 +
(1 - (p.embedding <-> sq.target_embedding)) * 3) AS hybrid_score
FROM
products p,
search_query sq
WHERE
p.tsv @@ sq.query_vector -- 全文检索匹配条件
AND p.embedding <-> sq.target_embedding < 2 -- 向量距离过滤条件
ORDER BY
hybrid_score DESC
LIMIT 10;
场景三: 引入热度和时效性的复合排序
需求 : 在相关性的基础上,考虑商品的流行度(如点击量)和时效性(如发布时间),推荐"爆款"和"新品"。
逻辑: 构建一个综合评分函数,包含语义相似度、热度(对数函数压缩)和时效性(指数衰减)三个因子。
sql
-- 假设 products 表中增加了 popularity (整数) 和 created_at (时间) 字段
-- ALTER TABLE products ADD COLUMN popularity INTEGER DEFAULT 0;
-- ALTER TABLE products ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
WITH search_context AS (
SELECT
'[1, 2, 3, 4, 5, 6, 7, 8]'::VECTOR AS target_embedding,
NOW() AS query_time
)
SELECT
p.id,
p.title,
p.price,
-- 复合评分逻辑:语义相似度(权重0.5) + 热度(权重0.25) + 时效性(权重0.25)
((1 - (p.embedding <-> sc.target_embedding)) * 0.5 +
LOG(GREATEST(p.popularity, 1) + 1) / LOG(10) * 0.25 +
EXP(-EXTRACT(EPOCH FROM (sc.query_time - p.created_at)) / 86400) * 0.25) AS final_score
FROM
products p,
search_context sc
ORDER BY
final_score DESC
LIMIT 20;
场景四: 基于用户画像的个性化推荐
需求 : 根据用户画像向量和行为数据,为用户推荐其可能感兴趣的商品。
逻辑: 首先,基于用户行为(如浏览、点击)召回候选商品类别;然后,根据用户画像向量与商品向量相似度进行精排。
sql
WITH user_context AS (
SELECT
up.user_id,
up.preference_vector,
-- 聚合用户行为,找出最常浏览的类别
(SELECT p.category
FROM user_events ue
JOIN products p ON ue.product_id = p.id
WHERE ue.user_id = 'user_tech'
GROUP BY p.category
ORDER BY COUNT(*) DESC
LIMIT 1) AS preferred_category
FROM user_profiles up
WHERE up.user_id = 'user_tech'
)
SELECT
p.id,
p.title,
p.category,
p.price,
(p.embedding <-> uc.preference_vector) AS similarity_distance
FROM
products p,
user_context uc
WHERE
p.category = uc.preferred_category -- 第一阶段: 行为召回
ORDER BY
p.embedding <-> uc.preference_vector -- 第二阶段: 向量精排
LIMIT 10;
API 速览
本部分汇总了上述示例中涉及的核心 PostgreSQL API。
1. pgvector 向量操作
- 类型 :
VECTOR(n)- 用于定义向量列。
n为维度。
- 用于定义向量列。
- 距离操作符 :
<->: L2 距离(欧几里得距离),值越小越相似。<=>: 余弦距离,值越小越相似。<#>: 负内积,值越小越相似。
- 索引加速 :
IVFFLAT: 基于倒排索引的近似最近邻搜索,构建速度快。HNSW: 基于分层可导航小世界图的索引,查询速度极快,但构建成本高。
代码示例:
sql
-- 创建向量列
ALTER TABLE products ADD COLUMN embedding VECTOR(384);
-- 使用 L2 距离查询
SELECT * FROM products ORDER BY embedding <-> '[0.1, 0.2, ...]'::VECTOR LIMIT 10;
-- 创建 IVFFlat 索引
CREATE INDEX ON products USING IVFFLAT (embedding vector_l2_ops) WITH (lists = 100);
2. 全文检索
- 类型 :
TSVECTOR,TSQUERYto_tsvector(): 将文本转换为TSVECTOR类型。to_tsquery(): 将查询词转换为TSQUERY类型。
- 操作符 :
@@: 匹配操作符,用于判断TSVECTOR是否满足TSQUERY。
- 函数 :
ts_rank(): 计算文本查询的相关度分数。
代码示例:
sql
-- 使用生成列自动创建 TSVECTOR
ALTER TABLE products ADD COLUMN tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || description)) STORED;
-- 创建 GIN 索引加速
CREATE INDEX ON products USING GIN (tsv);
-- 执行全文检索查询
SELECT title, ts_rank(tsv, to_tsquery('english', 'smartphone & camera')) AS rank
FROM products
WHERE tsv @@ to_tsquery('english', 'smartphone & camera')
ORDER BY rank DESC;
3. 通用函数
LOG(): 计算自然对数,常用于压缩大数值范围。EXP(): 指数函数,常用于实现时间衰减。EXTRACT(EPOCH FROM ...): 从时间戳中提取秒数,便于进行时间差计算。
Demo 完整示例
本示例提供一个基于 Node.js 和 pg 客户端的完整演示程序,模拟上述实时推荐与搜索的核心流程。
运行说明
- 环境准备 :
- 安装 Node.js (v16+) 和 npm。
- 准备一个运行中的 PostgreSQL 数据库实例,并已安装
pgvector扩展。
- 配置数据库 :
- 执行前文"环境准备"章节中的 SQL 脚本,创建表结构、函数并插入演示数据。
- 安装依赖 :
- 创建项目目录,并执行
npm init -y。 - 安装必要依赖:
npm install pg
- 创建项目目录,并执行
- 运行脚本 :
- 将以下代码保存为
demo.js,根据实际情况修改数据库连接配置,然后执行node demo.js。
- 将以下代码保存为
代码说明
以下 demo.js 脚本依次执行场景一、场景二和场景四的查询,并输出结果。
ts
// demo.js
const { Client } = require('pg');
// 数据库连接配置
const config = {
user: 'your_user',
host: '127.0.0.1',
database: 'your_db',
password: 'your_password',
port: 5432,
};
const client = new Client(config);
// 辅助函数: 生成随机向量 (模拟)
function generateRandomVector(dim) {
const arr = [];
for (let i = 0; i < dim; i++) {
arr.push(Math.random() * 100);
}
return `[${arr.join(',')}]`;
}
async function runDemo() {
try {
await client.connect();
console.log('数据库连接成功');
// 1. 场景一: 业务过滤 + 向量 Top-K
console.log('\n--- 场景一: 业务过滤 + 向量 Top-K ---');
const targetVector = generateRandomVector(8);
const query1 = `
SELECT id, title, price, embedding <-> $1::VECTOR AS distance
FROM products
WHERE category = '手机' AND price BETWEEN 500 AND 1500
ORDER BY embedding <-> $1::VECTOR
LIMIT 10;
`;
const res1 = await client.query(query1, [targetVector]);
console.log('查询结果:', res1.rows.map(r => ({ id: r.id, title: r.title, price: r.price, distance: r.distance })));
// 2. 场景二: 全文检索 + 语义检索混合排序
console.log('\n--- 场景二: 全文检索 + 语义检索混合排序 ---');
const query2 = `
WITH search_query AS (
SELECT to_tsquery('simple', 'iphone | camera') AS query_vector,
$1::VECTOR AS target_embedding
)
SELECT id, title, price,
(ts_rank(p.tsv, sq.query_vector) * 2 + (1 - (p.embedding <-> sq.target_embedding)) * 3) AS hybrid_score
FROM products p, search_query sq
WHERE p.tsv @@ sq.query_vector
ORDER BY hybrid_score DESC
LIMIT 10;
`;
const res2 = await client.query(query2, [targetVector]);
console.log('混合查询结果:', res2.rows.map(r => ({ id: r.id, title: r.title, price: r.price, score: r.hybrid_score })));
// 3. 场景四: 个性化推荐 (基于用户画像)
console.log('\n--- 场景四: 个性化推荐 ---');
const userId = 'user_tech';
const query4 = `
WITH user_context AS (
SELECT up.preference_vector, p.category
FROM user_profiles up
JOIN user_events ue ON up.user_id = ue.user_id
JOIN products p ON ue.product_id = p.id
WHERE up.user_id = $1
GROUP BY up.preference_vector, p.category
ORDER BY COUNT(*) DESC
LIMIT 1
)
SELECT p.id, p.title, p.category, p.price,
(p.embedding <-> uc.preference_vector) AS similarity_distance
FROM products p, user_context uc
WHERE p.category = uc.category
ORDER BY p.embedding <-> uc.preference_vector
LIMIT 10;
`;
const res4 = await client.query(query4, [userId]);
console.log('个性化推荐结果:', res4.rows.map(r => ({ id: r.id, title: r.title, category: r.category, price: r.price })));
} catch (err) {
console.error('执行出错:', err);
} finally {
await client.end();
}
}
runDemo();
技术点总结
- 单库多模 : 演示了如何在同一个
PostgreSQL数据库中同时处理结构化数据、全文检索和向量检索。 - 查询简化 : 通过
SQL将复杂的多阶段推荐逻辑(召回、过滤、排序)整合为单一数据库操作,减少了应用层代码量和数据往返。 - 索引加速 : 通过创建
GIN和IVFFLAT/HNSW索引,保障了查询在万级数据量下的毫秒级响应。 - 灵活排序 : 展示了如何使用
SQL函数自由组合向量距离、文本相关度、热度和时效性因子,实现高度定制化的排序策略。
多语言示例
Go 语言示例
本示例使用 Go 语言和 pgx 驱动(pgx/v5),实现与 Node.js 版本相同的实时推荐与搜索演示。
运行说明
- 环境准备 :
- 安装 Go 1.21+。
- 准备 PostgreSQL 数据库实例,已安装
pgvector扩展,并完成前文"环境准备"章节中的表结构与数据初始化。
- 项目初始化 :
- 创建项目目录,执行
go mod init demo。 - 安装依赖:
go get github.com/jackc/pgx/v5
- 创建项目目录,执行
- 运行脚本 :
- 将以下代码保存为
main.go,修改数据库连接字符串,执行go run main.go。
- 将以下代码保存为
代码说明
go
// main.go
package main
import (
"context"
"fmt"
"log"
"math/rand"
"strconv"
"strings"
"time"
"github.com/jackc/pgx/v5"
)
func main() {
// 数据库连接配置
connStr := "postgresql://your_user:your_password@127.0.0.1:5432/your_db"
ctx := context.Background()
conn, err := pgx.Connect(ctx, connStr)
if err != nil {
log.Fatal("数据库连接失败:", err)
}
defer conn.Close(ctx)
fmt.Println("数据库连接成功")
// 辅助函数:生成随机向量字符串
generateVector := func(dim int) string {
vals := make([]string, dim)
for i := 0; i < dim; i++ {
vals[i] = strconv.FormatFloat(rand.Float64()*100, 'f', 4, 64)
}
return "[" + strings.Join(vals, ",") + "]"
}
targetVec := generateVector(8)
// 场景一:业务过滤 + 向量 Top-K
fmt.Println("\n--- 场景一: 业务过滤 + 向量 Top-K ---")
query1 := `
SELECT id, title, price, embedding <-> $1::VECTOR AS distance
FROM products
WHERE category = '手机' AND price BETWEEN 500 AND 1500
ORDER BY embedding <-> $1::VECTOR
LIMIT 10;
`
rows, err := conn.Query(ctx, query1, targetVec)
if err != nil {
log.Fatal("查询失败:", err)
}
defer rows.Close()
fmt.Println("查询结果:")
for rows.Next() {
var id int
var title string
var price float64
var distance float64
rows.Scan(&id, &title, &price, &distance)
fmt.Printf("ID: %d, Title: %s, Price: %.2f, Distance: %.4f\n", id, title, price, distance)
}
// 场景二:全文检索 + 语义检索混合排序
fmt.Println("\n--- 场景二: 全文检索 + 语义检索混合排序 ---")
query2 := `
WITH search_query AS (
SELECT to_tsquery('simple', 'iphone | camera') AS query_vector,
$1::VECTOR AS target_embedding
)
SELECT id, title, price,
(ts_rank(p.tsv, sq.query_vector) * 2 + (1 - (p.embedding <-> sq.target_embedding)) * 3) AS hybrid_score
FROM products p, search_query sq
WHERE p.tsv @@ sq.query_vector
ORDER BY hybrid_score DESC
LIMIT 10;
`
rows2, err := conn.Query(ctx, query2, targetVec)
if err != nil {
log.Fatal("查询失败:", err)
}
defer rows2.Close()
fmt.Println("混合查询结果:")
for rows2.Next() {
var id int
var title string
var price float64
var score float64
rows2.Scan(&id, &title, &price, &score)
fmt.Printf("ID: %d, Title: %s, Price: %.2f, Score: %.4f\n", id, title, price, score)
}
// 场景四:个性化推荐
fmt.Println("\n--- 场景四: 个性化推荐 ---")
userId := "user_tech"
query4 := `
WITH user_context AS (
SELECT up.preference_vector, p.category
FROM user_profiles up
JOIN user_events ue ON up.user_id = ue.user_id
JOIN products p ON ue.product_id = p.id
WHERE up.user_id = $1
GROUP BY up.preference_vector, p.category
ORDER BY COUNT(*) DESC
LIMIT 1
)
SELECT p.id, p.title, p.category, p.price,
(p.embedding <-> uc.preference_vector) AS similarity_distance
FROM products p, user_context uc
WHERE p.category = uc.category
ORDER BY p.embedding <-> uc.preference_vector
LIMIT 10;
`
rows4, err := conn.Query(ctx, query4, userId)
if err != nil {
log.Fatal("查询失败:", err)
}
defer rows4.Close()
fmt.Println("个性化推荐结果:")
for rows4.Next() {
var id int
var title string
var category string
var price float64
var distance float64
rows4.Scan(&id, &title, &category, &price, &distance)
fmt.Printf("ID: %d, Title: %s, Category: %s, Price: %.2f, Distance: %.4f\n", id, title, category, price, distance)
}
}
技术点总结
- 驱动选择 :使用
pgx/v5,这是 Go 生态中性能优异的 PostgreSQL 驱动,支持pgvector的向量类型传递(通过$1::VECTOR显式转换)。 - 上下文管理 :使用
context.Context控制查询超时和取消。 - 类型映射 :
Scan方法直接将查询结果映射到 Go 基本类型,简单高效。 - 连接管理 :使用
defer确保连接和 Rows 正确关闭,防止资源泄露。
Python 语言示例
本示例使用 Python 和 psycopg2 驱动,实现相同的演示逻辑。
运行说明
- 环境准备 :
- 安装 Python 3.9+。
- 准备 PostgreSQL 数据库实例(含
pgvector和初始化数据)。
- 安装依赖 :
pip install psycopg2-binary
- 运行脚本 :
- 将以下代码保存为
demo.py,修改连接字符串,执行python demo.py。
- 将以下代码保存为
代码说明
py
# demo.py
import psycopg2
import random
def generate_vector(dim):
return '[' + ','.join([f'{random.random() * 100:.4f}' for _ in range(dim)]) + ']'
def main():
# 数据库连接
conn = psycopg2.connect(
host="127.0.0.1",
port=5432,
database="your_db",
user="your_user",
password="your_password"
)
cur = conn.cursor()
print("数据库连接成功")
target_vec = generate_vector(8)
# 场景一
print("\n--- 场景一: 业务过滤 + 向量 Top-K ---")
cur.execute("""
SELECT id, title, price, embedding <-> %s::VECTOR AS distance
FROM products
WHERE category = '手机' AND price BETWEEN 500 AND 1500
ORDER BY embedding <-> %s::VECTOR
LIMIT 10;
""", (target_vec, target_vec))
rows = cur.fetchall()
for row in rows:
print(f"ID: {row[0]}, Title: {row[1]}, Price: {row[2]:.2f}, Distance: {row[3]:.4f}")
# 场景二
print("\n--- 场景二: 全文检索 + 语义检索混合排序 ---")
cur.execute("""
WITH search_query AS (
SELECT to_tsquery('simple', 'iphone | camera') AS query_vector,
%s::VECTOR AS target_embedding
)
SELECT id, title, price,
(ts_rank(p.tsv, sq.query_vector) * 2 + (1 - (p.embedding <-> sq.target_embedding)) * 3) AS hybrid_score
FROM products p, search_query sq
WHERE p.tsv @@ sq.query_vector
ORDER BY hybrid_score DESC
LIMIT 10;
""", (target_vec,))
rows = cur.fetchall()
for row in rows:
print(f"ID: {row[0]}, Title: {row[1]}, Price: {row[2]:.2f}, Score: {row[3]:.4f}")
# 场景四
print("\n--- 场景四: 个性化推荐 ---")
user_id = "user_tech"
cur.execute("""
WITH user_context AS (
SELECT up.preference_vector, p.category
FROM user_profiles up
JOIN user_events ue ON up.user_id = ue.user_id
JOIN products p ON ue.product_id = p.id
WHERE up.user_id = %s
GROUP BY up.preference_vector, p.category
ORDER BY COUNT(*) DESC
LIMIT 1
)
SELECT p.id, p.title, p.category, p.price,
(p.embedding <-> uc.preference_vector) AS similarity_distance
FROM products p, user_context uc
WHERE p.category = uc.category
ORDER BY p.embedding <-> uc.preference_vector
LIMIT 10;
""", (user_id,))
rows = cur.fetchall()
for row in rows:
print(f"ID: {row[0]}, Title: {row[1]}, Category: {row[2]}, Price: {row[3]:.2f}, Distance: {row[4]:.4f}")
cur.close()
conn.close()
if __name__ == "__main__":
main()
技术点总结
- 驱动选择 :
psycopg2是 Python 最成熟的 PostgreSQL 适配器,支持%s占位符和显式类型转换(%s::VECTOR)。 - 参数化查询:使用元组传递参数,自动处理引号和转义,防止 SQL 注入。
- 简洁性 :Python 语法简洁,
fetchall()快速获取结果集,适合快速原型开发。 - 游标管理:手动关闭游标和连接,确保资源释放。
Java 语言示例
本示例使用 Java 和 JDBC 驱动(org.postgresql:postgresql),实现相同功能。
运行说明
-
环境准备 :
- 安装 JDK 17+。
- 准备 PostgreSQL 数据库实例(含
pgvector和初始化数据)。
-
项目配置 :
- 使用 Maven 或 Gradle 添加依赖(见下方
pom.xml)。 - 将以下代码保存为
Demo.java,编译运行。
- 使用 Maven 或 Gradle 添加依赖(见下方
-
依赖坐标 (Maven):
xml<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.2</version> </dependency>
代码说明
java
// Demo.java
import java.sql.*;
import java.util.Random;
public class Demo {
private static String generateVector(int dim) {
Random rand = new Random();
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < dim; i++) {
if (i > 0) sb.append(",");
sb.append(String.format("%.4f", rand.nextFloat() * 100));
}
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
String url = "jdbc:postgresql://127.0.0.1:5432/your_db";
String user = "your_user";
String password = "your_password";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
System.out.println("数据库连接成功");
String targetVec = generateVector(8);
// 场景一
System.out.println("\n--- 场景一: 业务过滤 + 向量 Top-K ---");
String sql1 = """
SELECT id, title, price, embedding <-> ?::VECTOR AS distance
FROM products
WHERE category = '手机' AND price BETWEEN 500 AND 1500
ORDER BY embedding <-> ?::VECTOR
LIMIT 10;
""";
try (PreparedStatement pstmt = conn.prepareStatement(sql1)) {
pstmt.setString(1, targetVec);
pstmt.setString(2, targetVec);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.printf("ID: %d, Title: %s, Price: %.2f, Distance: %.4f%n",
rs.getInt("id"), rs.getString("title"),
rs.getDouble("price"), rs.getDouble("distance"));
}
}
// 场景二
System.out.println("\n--- 场景二: 全文检索 + 语义检索混合排序 ---");
String sql2 = """
WITH search_query AS (
SELECT to_tsquery('simple', 'iphone | camera') AS query_vector,
?::VECTOR AS target_embedding
)
SELECT id, title, price,
(ts_rank(p.tsv, sq.query_vector) * 2 + (1 - (p.embedding <-> sq.target_embedding)) * 3) AS hybrid_score
FROM products p, search_query sq
WHERE p.tsv @@ sq.query_vector
ORDER BY hybrid_score DESC
LIMIT 10;
""";
try (PreparedStatement pstmt = conn.prepareStatement(sql2)) {
pstmt.setString(1, targetVec);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.printf("ID: %d, Title: %s, Price: %.2f, Score: %.4f%n",
rs.getInt("id"), rs.getString("title"),
rs.getDouble("price"), rs.getDouble("hybrid_score"));
}
}
// 场景四
System.out.println("\n--- 场景四: 个性化推荐 ---");
String sql4 = """
WITH user_context AS (
SELECT up.preference_vector, p.category
FROM user_profiles up
JOIN user_events ue ON up.user_id = ue.user_id
JOIN products p ON ue.product_id = p.id
WHERE up.user_id = ?
GROUP BY up.preference_vector, p.category
ORDER BY COUNT(*) DESC
LIMIT 1
)
SELECT p.id, p.title, p.category, p.price,
(p.embedding <-> uc.preference_vector) AS similarity_distance
FROM products p, user_context uc
WHERE p.category = uc.category
ORDER BY p.embedding <-> uc.preference_vector
LIMIT 10;
""";
try (PreparedStatement pstmt = conn.prepareStatement(sql4)) {
pstmt.setString(1, "user_tech");
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.printf("ID: %d, Title: %s, Category: %s, Price: %.2f, Distance: %.4f%n",
rs.getInt("id"), rs.getString("title"),
rs.getString("category"), rs.getDouble("price"),
rs.getDouble("similarity_distance"));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
技术点总结
- JDBC 标准 :使用
java.sql包,兼容所有 PostgreSQL JDBC 驱动。 - 文本块语法 :Java 15+ 支持
"""多行字符串,提高 SQL 可读性。 - PreparedStatement :使用
?占位符,通过setString传递向量参数,类型由::VECTOR在 SQL 中转换。 - 资源管理:使用 try-with-resources 自动关闭连接和语句,避免内存泄露。
- 格式化输出 :使用
System.out.printf美化输出。
多语言对比总结
| 特性 | Node.js (pg) | Go (pgx) | Python (psycopg2) | Java (JDBC) |
|---|---|---|---|---|
| 驱动/库 | pg |
github.com/jackc/pgx/v5 |
psycopg2-binary |
org.postgresql:postgresql |
| 参数占位符 | $1, $2 |
$1, $2 |
%s |
? |
| 向量参数传递 | 直接作为字符串,SQL 中 ::VECTOR |
直接作为字符串,SQL 中 ::VECTOR |
直接作为字符串,SQL 中 ::VECTOR |
直接作为字符串,SQL 中 ::VECTOR |
| 错误处理 | try-catch / async-await | error 返回值 | try-except | try-catch / SQLException |
| 连接管理 | 客户端手动 close | defer conn.Close() |
手动 close | try-with-resources |
| 依赖大小 | 轻量 (纯 JS) | 轻量 (静态编译) | 中等 (C 扩展) | 中等 (JDBC 驱动) |
| 异步支持 | 原生 async/await | 支持 pgx 原生异步 | 支持异步 (asyncpg) | 支持异步 (R2DBC) |
| 类型安全性 | 弱类型 (运行时) | 强类型 (编译时) | 弱类型 (运行时) | 强类型 (编译时) |
| 适用场景 | 快速原型、全栈 JS | 高并发、微服务 | 数据分析、脚本自动化 | 企业级应用、Spring 生态 |
各语言在实现相同业务逻辑时的核心差异在于驱动 API 风格、错误处理机制和类型系统,但对 PostgreSQL + pgvector 的 SQL 语义支持完全一致,均可通过标准 SQL 完成实时推荐与搜索的混合查询任务。
项目难点与解决方案
核心难点
- 混合查询性能调优 : 当
WHERE子句中同时包含业务过滤条件和向量距离计算时,优化器可能无法选择最优索引,导致查询性能下降。 - 索引选择与参数调优 :
IVFFLAT和HNSW索引有不同的适用场景和参数(如lists,m,ef_construction),不当配置会影响召回率或查询速度。 - 向量维度与存储成本: 高维向量(如1536维)会显著增加存储开销和计算时间,需在精度和资源间取得平衡。
解决方案
- 查询规划调优 : 使用
CTE或子查询强制将业务过滤和向量检索分离执行,或通过设置enable_seqscan等参数(谨慎使用)引导优化器选择索引扫描。 - 索引实验与监控 : 在实际数据分布下,对比
IVFFLAT和HNSW在不同参数下的查询延迟和召回率,选择最佳配置。例如,调整IVFFLAT的lists参数约为rows / 1000。 - 向量降维与量化 : 在应用层考虑使用更高效的嵌入模型或降维技术(如 PCA)。
pgvector也支持halfvec类型以降低存储开销。
广度
涵盖了构建实时推荐系统所需的多方面技术,包括数据库设计、索引优化、全文检索、向量检索、排序算法设计以及与后端应用的集成。
深度
深入探讨了 PostgreSQL 内部机制,如生成列、多种索引类型的实现原理与适用场景、SQL 层复杂评分函数的构建,以及查询性能的调优策略。
复杂度
面对混合检索、个性化推荐和实时性要求,系统复杂度较高。通过将所有逻辑集中在数据库层,有效降低了应用层和系统整体的复杂度,但同时也对数据库管理和 SQL 编写能力提出了更高要求。
官方文档
- PostgreSQL 官方文档 : https://www.postgresql.org/docs/current/
- pgvector 官方文档 : https://github.com/pgvector/pgvector
- PostgreSQL 全文检索文档 : https://www.postgresql.org/docs/current/textsearch.html
参考链接
- PgBouncer 连接池 : https://www.pgbouncer.org/
- PostgreSQL 性能调优 wiki : https://wiki.postgresql.org/wiki/Performance_Optimization
- 使用 Postgres 进行相似性搜索 : https://supabase.com/blog/vector-search-ai-pgvector
总结
本文详细阐述了如何利用 PostgreSQL 及其扩展 pgvector 构建一个功能完备的实时推荐与搜索系统。我们深入探讨了从数据建模、索引创建到复杂混合查询(融合业务过滤、全文检索、向量相似度、热度及时效性排序)的全过程。通过具体的 SQL 示例和 Node.js Demo 演示,证明了 PostgreSQL 作为一个多模态数据库,能够有效整合传统检索与 AI 向量检索能力,简化技术栈,并在中小规模场景下提供卓越的实时性能。其核心价值在于将复杂的召回、过滤与排序逻辑整合于数据库内核,为开发者提供了一种高效、简洁的解决方案。