用 Java 做 RAG,旁边挂一个向量库是默认动作:Milvus、Qdrant、pgvector 随便挑,多一个容器、多一份监控备份。
Spring AI 自带的 SimpleVectorStore 是参考实现,底下就一个 ConcurrentHashMap。所谓向量搜索就是全表线性扫描,每条向量都比一遍,没有索引。持久化也就是把 Map 写成 JSON。语料一多,查询开始停顿。
integrallis/vectors 补的正是中间这块。不用另起服务,也有真正的向量索引。Embedding 走 PigAI,检索留在进程里。
先认识 Vector API Vector API 是 JDK 里做 SIMD 向量运算的一套 API,至今还在孵化,藏在 jdk.incubator.vector 模块里。JDK 27 是它的第 12 次孵化,启动要加 --add-modules jdk.incubator.vector。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static float dot (float [] a, float [] b) { var s = FloatVector.SPECIES_PREFERRED; float sum = 0f ; int i = 0 ; for (; i < s.loopBound(a.length); i += s.length()) { sum += FloatVector.fromArray(s, a, i) .mul(FloatVector.fromArray(s, b, i)) .reduceLanes(VectorOperators.ADD); } for (; i < a.length; i++) { sum += a[i] * b[i]; } return sum; }
它的痛点是太原子化。你要的是「点积」这个结果,它只给你 fromArray、mul、reduceLanes 这些零件,其他都需要参考如上这种去实现。integrallis/vectors 把这些零件封成了距离核、索引、持久化三样,直接用。
integrallis/vectors 封装了什么 integrallis/vectors 把 Vector API 那堆零件封成了三样,接口还是 add / search 那套:
距离核 :点积、L2、余弦都现成,能走 SIMD 就走,走不了回退标量。
索引 :FLAT 还在(语料小、要精确结果用),日常用 HNSW,另外有 IVF_FLAT、IVF_PQ。量化有 SQ8 / SQ4、PQ、RaBitQ。
持久化 :向量走 MemorySegment,commit() 写落盘,重启不用重新 embed。
1 2 3 4 5 <dependency > <groupId > com.integrallis</groupId > <artifactId > vectors</artifactId > <version > 0.1.7</version > </dependency >
接入 Spring AI classpath 上有 EmbeddingModel,starter 就配好 VectorCollection 和 VectorStore。
1 2 3 4 5 <dependency > <groupId > com.integrallis</groupId > <artifactId > vectors-spring-boot-starter</artifactId > <version > 0.1.7</version > </dependency >
默认索引 FLAT,改 HNSW 就写配置。commit-after-add: true 让每次 add 都落盘,关掉就得手动 commit 才查得到:
1 2 3 4 java-vectors: index-type: HNSW storage-path: /var/lib/pigai/vectors/kb commit-after-add: true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Autowired VectorStore vectorStore; public void ingest () { vectorStore.add(List.of( new Document ( "浏览器报错 404,请检测您输入的路径是否正确" , Map.of("author" , "lengleng" , "product" , "PigAI" )), new Document ( "host 报错请检查环境" , Map.of("author" , "lengleng" , "product" , "PigAI" )))); } public List<Document> ask (String question) { return vectorStore.similaritySearch( SearchRequest.builder() .query(question) .topK(5 ) .filterExpression("author == 'lengleng'" ) .build()); }
调用点跟 pgvector、Qdrant 一样,filterExpression 还是 Spring AI 那串表达式。
接入 LangChain4j 1 2 3 4 5 <dependency > <groupId > com.integrallis</groupId > <artifactId > vectors-langchain4j</artifactId > <version > 0.1.7</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 VectorCollection collection = VectorCollection.builder() .dimension(embeddingModel.dimension()) .metric(SimilarityFunction.COSINE) .indexType(IndexType.HNSW) .storagePath(Path.of("/var/lib/pigai/vectors/kb" )) .build(); EmbeddingStore<TextSegment> store = JavaVectorsEmbeddingStore.builder(collection) .commitAfterAdd(true ) .build(); TextSegment segment = TextSegment.from( "PigAI 知识库检索走进程内向量集合" , Metadata.from("author" , "lengleng" )); store.add(embeddingModel.embed(segment).content(), segment); var matches = store.search( EmbeddingSearchRequest.builder() .queryEmbedding(embeddingModel.embed("进程内怎么查" ).content()) .maxResults(5 ) .filter(metadataKey("author" ).isEqualTo("lengleng" )) .build());
两套框架(Spring AI / LangChain4j)可以指向同一个 storagePath,但同一时刻只能一个 writer。
启动参数和边界 两个 JVM 参数要写进镜像的 JAVA_TOOL_OPTIONS 和 CI,别只留在本地 IDEA:
1 2 --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED