JVM 原生向量能力,让 RAG 检索少一个容器

用 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;
}

它的痛点是太原子化。你要的是「点积」这个结果,它只给你 fromArraymulreduceLanes 这些零件,其他都需要参考如上这种去实现。integrallis/vectors 把这些零件封成了距离核、索引、持久化三样,直接用。

integrallis/vectors 封装了什么

integrallis/vectors 把 Vector API 那堆零件封成了三样,接口还是 add / search 那套:

  • 距离核:点积、L2、余弦都现成,能走 SIMD 就走,走不了回退标量。
  • 索引:FLAT 还在(语料小、要精确结果用),日常用 HNSW,另外有 IVF_FLAT、IVF_PQ。量化有 SQ8 / SQ4、PQ、RaBitQ。
  • 持久化:向量走 MemorySegmentcommit() 写落盘,重启不用重新 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 就配好 VectorCollectionVectorStore

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