使用 PostgreSQL + pgvector 实现 RAG 向量存储与语义检索(Java 实战)
在 RAG(Retrieval-Augmented Generation)系统中,向量存储与相似度检索 是最核心的一环。
本文将使用 PostgreSQL + pgvector ,结合 Java + 阿里百炼 Embedding 模型,实现一个完整、可运行的向量存储与语义检索示例。
本文不仅关注"能跑",更重点解释 为什么这么设计 以及 RAG 中最容易踩的坑。
一、pgvector 是什么?
pgvector 是 PostgreSQL 的一个扩展插件,为 PostgreSQL 提供了专门的 vector 类型,用于存储高维向量,并支持:
- 余弦距离(cosine distance)
- 欧氏距离(L2)
- 内积(inner product)
- 向量索引(ivfflat / hnsw)
这使得 PostgreSQL 可以直接作为 向量数据库 使用,非常适合中小规模 RAG 场景。
二、安装 PostgreSQL 与 pgvector
安装过程不再赘述。
👉 可参考 CSDN 博主 进击的女IT 的文章:
https://blog.csdn.net/weixin_63908159/article/details/156075242
三、建表与索引设计(非常重要)
1️⃣ 建表语句
sql
CREATE TABLE document (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1024),
create_time TIMESTAMP DEFAULT now()
);
⚠️ 注意
vector(1024)必须与 Embedding 模型输出的向量维度一致。如果模型输出是 1536 维,这里必须改成vector(1536),否则插入时会报错:expected xxx dimensions。
2️⃣ 向量索引(否则数据一多会非常慢)
sql
CREATE INDEX idx_document_embedding
ON document
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
查询前建议设置:
sql
SET ivfflat.probes = 10;
四、Java 实体类设计
pgvector 在 Java 中无需特殊类型映射,直接使用 String 承载即可。
java
@Data
public class Document {
private Long id;
private String content;
/**
* pgvector 字段
* 使用 String 承载,例如:[0.12,0.34,...]
*/
private String embedding;
private LocalDateTime createTime;
}
五、Embedding 模型配置(阿里百炼)
配置文件 (application.yml):
yml
alibaba:
dashscope:
key: sk-xxx
url: https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings
model: text-embedding-v4
配置类:
java
@Configuration
@ConfigurationProperties(prefix = "alibaba.dashscope")
@Data
public class AlibabaDashscopeConfig {
private String key;
private String url;
private String model;
}
六、文本向量化服务(EmbeddingService)
功能:文本 → 向量 (List<Float>)
java
@Service
@Slf4j
public class EmbeddingService {
@Autowired
private AlibabaDashscopeConfig config;
public List<Float> getEmbedding(String text) {
if (StrUtil.isBlank(text)) {
return List.of();
}
Map<String, Object> body = Map.of(
"model", config.getModel(),
"input", List.of(text)
);
HttpResponse response = HttpRequest.post(config.getUrl())
.header("Authorization", "Bearer " + config.getKey())
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(body))
.timeout(30000)
.execute();
JSONObject json = JSONUtil.parseObj(response.body());
JSONArray embedding = json.getJSONArray("data")
.getJSONObject(0)
.getJSONArray("embedding");
return embedding.toList(Float.class);
}
}
七、向量存储与相似度查询
Mapper
java
@Mapper
public interface DocumentMapper extends BaseMapper<Document> {
/**
* 向量相似度搜索(余弦距离)
*/
@Select("""
SELECT id, content
FROM document
ORDER BY embedding <=> #{embedding}::vector
LIMIT #{limit}
""")
List<Document> searchByEmbedding(
@Param("embedding") String embedding,
@Param("limit") int limit
);
@Insert("""
INSERT INTO document (content, embedding)
VALUES (#{content}, #{embedding}::vector)
""")
void insertDocument(
@Param("content") String content,
@Param("embedding") String embedding
);
}
Service
java
@Service
@RequiredArgsConstructor
public class DocumentService {
private final DocumentMapper mapper;
private final EmbeddingService embeddingService;
public void addDocument(String content) {
List<Float> vector = embeddingService.getEmbedding(content);
String pgVector = vector.stream()
.map(String::valueOf)
.collect(Collectors.joining(",", "[", "]"));
mapper.insertDocument(content, pgVector);
}
public List<Document> search(String query, int topK) {
List<Float> vector = embeddingService.getEmbedding(query);
if (vector.isEmpty()) {
return List.of();
}
String pgVector = vector.stream()
.map(String::valueOf)
.collect(Collectors.joining(",", "[", "]"));
return mapper.searchByEmbedding(pgVector, topK);
}
}
八、Controller 接口
java
@RestController
@RequestMapping("/document")
@RequiredArgsConstructor
public class DocumentController {
private final DocumentService service;
@GetMapping("/add")
public void add(@RequestParam String content) {
service.addDocument(content);
}
@GetMapping("/search")
public List<Document> search(
@RequestParam String query,
@RequestParam(defaultValue = "5") int topK
) {
return service.search(query, topK);
}
}
九、为什么"最近流行什么"查询不到结果?
这是语义检索中最容易被误解的一点:
向量检索 ≠ 关键词匹配
向量检索判断的是 语义是否在同一语义空间。
示例:
| 文本 | 语义中心 |
|---|---|
| Java 是一门流行的后端开发语言 | 编程 / 后端 |
| 最近流行什么 | 趋势 / 热点 |
👉 两者在语义空间中的距离很远,因此检索不到是正常且正确的行为。
正确做法:
- Query Rewrite(查询重写)
- 补充领域上下文
例如:
最近流行的后端开发语言有哪些?
十、总结
pgvector可以让 PostgreSQL 直接作为向量数据库使用。- 向量检索本质是 语义相似度计算。
- RAG 的效果高度依赖于:
- 文档内容的表达方式
- Query 是否足够具体
- 相似度阈值与 TopK 的设计
📌 项目完整源码地址(Gitee)
https://gitee.com/tfxing12138/rag-demo.git