Java调用Python实现FAISS向量操作(两种方式完整实战)

「鸿蒙心迹」“2025・领航者闯关记“主题征文活动 10w+人浏览 604人参与

Java调用Python实现FAISS向量操作(两种方式完整实战)

由于FAISS尚未提供Java客户端版本,我们采用Python作为中间层来实现功能。

一、场景背景

本文围绕“Java业务端调用Python实现FAISS向量存储核心功能(添加、检索、清空)”展开,提供HTTP接口调用(推荐)和本地进程调用两种实现方案,包含完整代码、部署步骤和联调验证,方便开发者根据场景选择。

核心目标

  • Python侧:实现FAISS向量操作(兼容模拟版/真实FAISS版),对外提供调用入口;
  • Java侧:分别通过HTTP和本地进程两种方式调用Python功能,完成向量文档的添加、检索、清空。

二、前置准备

1. 环境依赖

环境版本要求备注
Python3.8+需安装faiss-cpu(真实FAISS)/flask(HTTP接口)
Java8+/17+Spring Boot项目(本文用2.7.x)
依赖安装pip install faiss-cpu flask numpyPython侧依赖

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接口,兼顾性能和解耦。

七、扩展优化

  1. Python侧:
    • 替换Flask为FastAPI,提升接口性能和并发能力;
    • 添加索引持久化(FAISS索引保存到文件,重启后加载);
    • 增加向量维度校验、权限控制等。
  2. Java侧:
    • 对RestTemplate做连接池配置,提升HTTP调用性能;
    • 进程调用时增加脚本超时控制,避免卡死;
    • 添加统一的异常处理和日志监控。
  3. 通用优化:
    • 向量数据传输时可做压缩(如Float32转Float16);
    • 生产环境添加接口熔断、降级(如Sentinel)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coder_Boy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值