Java调用Python实现FAISS向量操作(两种方式完整实战)
由于FAISS尚未提供Java客户端版本,我们采用Python作为中间层来实现功能。
一、场景背景
本文围绕“Java业务端调用Python实现FAISS向量存储核心功能(添加、检索、清空)”展开,提供HTTP接口调用(推荐)和本地进程调用两种实现方案,包含完整代码、部署步骤和联调验证,方便开发者根据场景选择。
核心目标
- Python侧:实现FAISS向量操作(兼容模拟版/真实FAISS版),对外提供调用入口;
- Java侧:分别通过HTTP和本地进程两种方式调用Python功能,完成向量文档的添加、检索、清空。
二、前置准备
1. 环境依赖
| 环境 | 版本要求 | 备注 |
|---|---|---|
| Python | 3.8+ | 需安装faiss-cpu(真实FAISS)/flask(HTTP接口) |
| Java | 8+/17+ | Spring Boot项目(本文用2.7.x) |
| 依赖安装 | pip install faiss-cpu flask numpy | Python侧依赖 |
2. 统一数据结构
Java和Python侧对齐VectorizedDocument结构:
- 核心字段:
id(文档唯一标识)、vector(浮点型向量数组); - 交互格式:HTTP方式用JSON,进程调用用JSON字符串/文本。
三、Python侧实现(FAISS核心功能)
1. 基础FAISS工具类(兼容两种调用方式)
新建faiss_core.py,封装FAISS核心操作(支持真实FAISS,也可切换模拟逻辑):
import os
import uuid
import faiss
import numpy as np
from typing import List, Dict, Optional
class VectorizedDocument:
"""向量文档类,对齐Java端数据结构"""
def __init__(self, doc_id: Optional[str] = None, vector: Optional[List[float]] = None):
self.id = doc_id or str(uuid.uuid4())
self.vector = vector or []
def to_dict(self):
"""转换为字典,方便JSON序列化"""
return {"id": self.id, "vector": self.vector}
@staticmethod
def from_dict(data: dict):
"""从字典反序列化"""
return VectorizedDocument(doc_id=data.get("id"), vector=data.get("vector"))
class FaissVectorStore:
"""FAISS向量存储核心类(真实FAISS实现)"""
def __init__(self, dimension: int = 384, index_path: str = "./faiss_index"):
self.dimension = dimension
self.index_path = index_path
self.index = None # FAISS索引对象
self.doc_id_map: Dict[int, str] = {} # FAISS内部ID -> 业务ID映射
self._init_index()
def _init_index(self):
"""初始化FAISS索引"""
# 创建索引目录
if not os.path.exists(self.index_path):
os.makedirs(self.index_path)
# 初始化FlatL2索引(适合中小数据量,精度高)
self.index = faiss.IndexFlatL2(self.dimension)
def add_documents(self, documents: List[VectorizedDocument]) -> str:
"""添加向量文档"""
if not documents:
return "无待添加的文档"
try:
vectors = []
for doc in documents:
# 校验向量维度
if len(doc.vector) != self.dimension:
raise ValueError(f"向量维度不匹配:预期{self.dimension},实际{len(doc.vector)}")
vectors.append(doc.vector)
self.doc_id_map[self.index.ntotal] = doc.id
# 转换为FAISS支持的float32数组
vec_array = np.array(vectors, dtype=np.float32)
self.index.add(vec_array)
return f"成功添加{len(documents)}个文档,当前总量:{self.index.ntotal}"
except Exception as e:
return f"添加失败:{str(e)}"
def search_documents(self, query_vector: List[float], top_k: int) -> List[VectorizedDocument]:
"""检索相似文档"""
if len(query_vector) != self.dimension:
raise ValueError(f"查询向量维度不匹配:预期{self.dimension}")
# 转换为numpy数组
query_array = np.array([query_vector], dtype=np.float32)
# FAISS检索(返回距离和内部ID)
distances, indices = self.index.search(query_array, top_k)
# 解析结果
results = []
for idx in indices[0]:
if idx == -1:
continue
doc_id = self.doc_id_map.get(idx)
if doc_id:
results.append(VectorizedDocument(doc_id=doc_id))
return results
def clear_documents(self) -> str:
"""清空所有文档"""
self.index = faiss.IndexFlatL2(self.dimension)
self.doc_id_map.clear()
return "已清空所有向量文档"
# 全局实例(方便接口调用)
faiss_store = FaissVectorStore(dimension=384)
2. 方案1:HTTP接口封装(推荐)
新建faiss_api.py,用Flask封装HTTP接口,供Java远程调用:
from flask import Flask, request, jsonify
from faiss_core import faiss_store, VectorizedDocument
app = Flask(__name__)
# 接口1:添加向量文档
@app.route("/faiss/add", methods=["POST"])
def add_documents():
try:
data = request.json
docs_data = data.get("documents", [])
# 转换为VectorizedDocument对象
documents = [VectorizedDocument.from_dict(doc) for doc in docs_data]
result = faiss_store.add_documents(documents)
return jsonify({"code": 200, "msg": result})
except Exception as e:
return jsonify({"code": 500, "msg": f"添加失败:{str(e)}"}), 500
# 接口2:检索相似文档
@app.route("/faiss/search", methods=["POST"])
def search_documents():
try:
data = request.json
query_vector = data.get("queryVector", [])
top_k = data.get("topK", 10)
results = faiss_store.search_documents(query_vector, top_k)
# 转换为JSON可序列化格式
results_dict = [doc.to_dict() for doc in results]
return jsonify({"code": 200, "data": results_dict})
except Exception as e:
return jsonify({"code": 500, "msg": f"检索失败:{str(e)}"}), 500
# 接口3:清空文档
@app.route("/faiss/clear", methods=["POST"])
def clear_documents():
try:
result = faiss_store.clear_documents()
return jsonify({"code": 200, "msg": result})
except Exception as e:
return jsonify({"code": 500, "msg": f"清空失败:{str(e)}"}), 500
if __name__ == "__main__":
# 启动Flask服务,允许外部访问
app.run(host="0.0.0.0", port=8000, debug=True)
3. 方案2:本地进程调用(脚本入参版)
新建faiss_cli.py,支持通过命令行参数调用功能,供Java本地执行脚本:
import sys
import json
from faiss_core import faiss_store, VectorizedDocument
def main():
# 入参格式:python faiss_cli.py <action> <params_json>
if len(sys.argv) < 3:
print(json.dumps({"code": 400, "msg": "参数不足:需传入action和params_json"}))
return
action = sys.argv[1]
params = json.loads(sys.argv[2])
try:
if action == "add":
docs_data = params.get("documents", [])
documents = [VectorizedDocument.from_dict(doc) for doc in docs_data]
result = faiss_store.add_documents(documents)
print(json.dumps({"code": 200, "msg": result}))
elif action == "search":
query_vector = params.get("queryVector", [])
top_k = params.get("topK", 10)
results = faiss_store.search_documents(query_vector, top_k)
results_dict = [doc.to_dict() for doc in results]
print(json.dumps({"code": 200, "data": results_dict}))
elif action == "clear":
result = faiss_store.clear_documents()
print(json.dumps({"code": 200, "msg": result}))
else:
print(json.dumps({"code": 400, "msg": f"不支持的操作:{action}"}))
except Exception as e:
print(json.dumps({"code": 500, "msg": f"执行失败:{str(e)}"}))
if __name__ == "__main__":
main()
四、Java侧实现(两种调用方式)
1. 基础准备
(1)数据模型
新建VectorizedDocument.java,对齐Python侧结构:
package com.example.ai.domain.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VectorizedDocument {
private String id;
private float[] vector;
// 自动生成ID
public VectorizedDocument(float[] vector) {
this.id = UUID.randomUUID().toString();
this.vector = vector;
}
}
(2)配置依赖(pom.xml)
添加HTTP请求和JSON解析依赖:
<!-- HTTP客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JSON解析 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<!-- 进程调用辅助 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
2. 方案1:HTTP接口调用(推荐)
新建FaissHttpClient.java,通过RestTemplate调用Python的HTTP接口:
package com.example.ai.infrastructure.vectorstore;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.example.ai.domain.model.VectorizedDocument;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
@Component
@Slf4j
public class FaissHttpClient {
// Python HTTP服务地址
private static final String PYTHON_API_URL = "http://localhost:8000";
private final RestTemplate restTemplate = new RestTemplate();
/**
* 调用Python HTTP接口添加向量文档
*/
public String addDocuments(List<VectorizedDocument> documents) {
try {
// 构造请求参数
JSONObject params = new JSONObject();
params.put("documents", JSON.toJSON(documents));
// 调用POST接口
String url = PYTHON_API_URL + "/faiss/add";
String response = restTemplate.postForObject(url, params, String.class);
// 解析响应
JSONObject result = JSON.parseObject(response);
if (result.getInteger("code") == 200) {
return result.getString("msg");
} else {
log.error("添加文档失败:{}", result.getString("msg"));
return "添加失败:" + result.getString("msg");
}
} catch (Exception e) {
log.error("HTTP调用添加接口异常", e);
return "添加异常:" + e.getMessage();
}
}
/**
* 调用Python HTTP接口检索相似文档
*/
public List<VectorizedDocument> searchDocuments(float[] queryVector, int topK) {
try {
// 构造请求参数
JSONObject params = new JSONObject();
params.put("queryVector", queryVector);
params.put("topK", topK);
// 调用POST接口
String url = PYTHON_API_URL + "/faiss/search";
String response = restTemplate.postForObject(url, params, String.class);
// 解析响应
JSONObject result = JSON.parseObject(response);
if (result.getInteger("code") == 200) {
JSONArray data = result.getJSONArray("data");
return JSON.parseArray(data.toJSONString(), VectorizedDocument.class);
} else {
log.error("检索文档失败:{}", result.getString("msg"));
return List.of();
}
} catch (Exception e) {
log.error("HTTP调用检索接口异常", e);
return List.of();
}
}
/**
* 调用Python HTTP接口清空文档
*/
public String clearDocuments() {
try {
String url = PYTHON_API_URL + "/faiss/clear";
String response = restTemplate.postForObject(url, new JSONObject(), String.class);
JSONObject result = JSON.parseObject(response);
if (result.getInteger("code") == 200) {
return result.getString("msg");
} else {
log.error("清空文档失败:{}", result.getString("msg"));
return "清空失败:" + result.getString("msg");
}
} catch (Exception e) {
log.error("HTTP调用清空接口异常", e);
return "清空异常:" + e.getMessage();
}
}
}
3. 方案2:本地进程调用
新建FaissProcessClient.java,通过ProcessBuilder执行Python脚本:
package com.example.ai.infrastructure.vectorstore;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.example.ai.domain.model.VectorizedDocument;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
@Component
@Slf4j
public class FaissProcessClient {
// Python脚本路径(根据实际路径修改)
private static final String PYTHON_SCRIPT_PATH = "D:/project/faiss_demo/faiss_cli.py";
// Python解释器路径(若已配置环境变量,直接写"python"即可)
private static final String PYTHON_EXEC = "python";
/**
* 执行Python脚本
*/
private String executePythonScript(String action, JSONObject params) {
ProcessBuilder pb = new ProcessBuilder(
PYTHON_EXEC,
PYTHON_SCRIPT_PATH,
action,
params.toJSONString()
);
// 重定向错误流到标准输出
pb.redirectErrorStream(true);
StringBuilder output = new StringBuilder();
try {
Process process = pb.start();
// 读取脚本输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line);
}
// 等待脚本执行完成
int exitCode = process.waitFor();
if (exitCode != 0) {
log.error("Python脚本执行失败,退出码:{},输出:{}", exitCode, output);
return null;
}
return output.toString();
} catch (IOException | InterruptedException e) {
log.error("执行Python脚本异常", e);
return null;
}
}
/**
* 进程调用添加文档
*/
public String addDocuments(List<VectorizedDocument> documents) {
JSONObject params = new JSONObject();
params.put("documents", JSON.toJSON(documents));
String response = executePythonScript("add", params);
if (StringUtils.isBlank(response)) {
return "添加失败:脚本执行无响应";
}
JSONObject result = JSON.parseObject(response);
if (result.getInteger("code") == 200) {
return result.getString("msg");
} else {
log.error("添加文档失败:{}", result.getString("msg"));
return "添加失败:" + result.getString("msg");
}
}
/**
* 进程调用检索文档
*/
public List<VectorizedDocument> searchDocuments(float[] queryVector, int topK) {
JSONObject params = new JSONObject();
params.put("queryVector", queryVector);
params.put("topK", topK);
String response = executePythonScript("search", params);
if (StringUtils.isBlank(response)) {
log.error("检索文档失败:脚本执行无响应");
return List.of();
}
JSONObject result = JSON.parseObject(response);
if (result.getInteger("code") == 200) {
JSONArray data = result.getJSONArray("data");
return JSON.parseArray(data.toJSONString(), VectorizedDocument.class);
} else {
log.error("检索文档失败:{}", result.getString("msg"));
return List.of();
}
}
/**
* 进程调用清空文档
*/
public String clearDocuments() {
String response = executePythonScript("clear", new JSONObject());
if (StringUtils.isBlank(response)) {
return "清空失败:脚本执行无响应";
}
JSONObject result = JSON.parseObject(response);
if (result.getInteger("code") == 200) {
return result.getString("msg");
} else {
log.error("清空文档失败:{}", result.getString("msg"));
return "清空失败:" + result.getString("msg");
}
}
}
4. 测试验证
新建FaissTestController.java,提供测试接口验证两种调用方式:
package com.example.ai.controller;
import com.example.ai.domain.model.VectorizedDocument;
import com.example.ai.infrastructure.vectorstore.FaissHttpClient;
import com.example.ai.infrastructure.vectorstore.FaissProcessClient;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/faiss/test")
@RequiredArgsConstructor
public class FaissTestController {
private final FaissHttpClient faissHttpClient;
private final FaissProcessClient faissProcessClient;
/**
* 测试HTTP方式添加文档
*/
@GetMapping("/http/add")
public String testHttpAdd() {
// 构造测试文档
VectorizedDocument doc1 = new VectorizedDocument(new float[]{1.0f, 2.0f, 3.0f});
VectorizedDocument doc2 = new VectorizedDocument(new float[]{4.0f, 5.0f, 6.0f});
return faissHttpClient.addDocuments(List.of(doc1, doc2));
}
/**
* 测试HTTP方式检索文档
*/
@GetMapping("/http/search")
public List<VectorizedDocument> testHttpSearch() {
float[] queryVector = new float[]{1.1f, 2.1f, 3.1f};
return faissHttpClient.searchDocuments(queryVector, 2);
}
/**
* 测试进程方式添加文档
*/
@GetMapping("/process/add")
public String testProcessAdd() {
VectorizedDocument doc1 = new VectorizedDocument(new float[]{1.0f, 2.0f, 3.0f});
VectorizedDocument doc2 = new VectorizedDocument(new float[]{4.0f, 5.0f, 6.0f});
return faissProcessClient.addDocuments(List.of(doc1, doc2));
}
/**
* 测试进程方式检索文档
*/
@GetMapping("/process/search")
public List<VectorizedDocument> testProcessSearch() {
float[] queryVector = new float[]{1.1f, 2.1f, 3.1f};
return faissProcessClient.searchDocuments(queryVector, 2);
}
}
五、部署与验证步骤
1. 启动Python服务
(1)HTTP方式
执行python faiss_api.py,启动Flask服务(默认端口8000),控制台输出:
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8000
(2)进程方式
无需启动服务,确保Python脚本路径和解释器路径正确即可。
2. 启动Java服务
运行Spring Boot项目,访问以下测试接口验证:
- HTTP添加:
http://localhost:8080/faiss/test/http/add - HTTP检索:
http://localhost:8080/faiss/test/http/search - 进程添加:
http://localhost:8080/faiss/test/process/add - 进程检索:
http://localhost:8080/faiss/test/process/search
六、两种方式对比与选型建议
| 维度 | HTTP接口调用 | 本地进程调用 |
|---|---|---|
| 部署方式 | Python独立服务,可远程调用 | Python脚本与Java同机部署 |
| 性能 | 网络开销,但支持高并发 | 无网络开销,但并发差(进程创建耗时) |
| 维护性 | 易维护,接口解耦 | 脚本路径/环境依赖易出问题 |
| 适用场景 | 生产环境、分布式部署、高并发 | 测试环境、单机部署、低频次调用 |
选型建议
- 生产环境:优先选择HTTP接口方式(推荐结合FastAPI/nginx做接口优化);
- 测试/单机场景:可选择进程调用方式,简化部署;
- 高性能需求:可将Python服务部署为gRPC接口,兼顾性能和解耦。
七、扩展优化
- Python侧:
- 替换Flask为FastAPI,提升接口性能和并发能力;
- 添加索引持久化(FAISS索引保存到文件,重启后加载);
- 增加向量维度校验、权限控制等。
- Java侧:
- 对RestTemplate做连接池配置,提升HTTP调用性能;
- 进程调用时增加脚本超时控制,避免卡死;
- 添加统一的异常处理和日志监控。
- 通用优化:
- 向量数据传输时可做压缩(如Float32转Float16);
- 生产环境添加接口熔断、降级(如Sentinel)。
2026

被折叠的 条评论
为什么被折叠?



