主题
CockroachDB
CockroachDB 是一个讲 PostgreSQL 线路协议的分布式 SQL 数据库。自 v24.2 起,它自带一个原生 VECTOR 列类型;自 v25.2 起,它提供一个称为 C-SPANN 的分布式近似最近邻索引。langchain4j-community-cockroachdb 模块把二者作为以下方式与 LangChain4j 集成:
- 一个向量
EmbeddingStore<TextSegment>(CockroachDbEmbeddingStore) - 一个
ChatMemoryStore(CockroachDbChatMemoryStore)
该 Java 模块在存在 Java 对应物的地方,镜像了官方 Python langchain-cockroachdb 库的特性集。
版本要求
| 特性 | CockroachDB 最低版本 |
|---|---|
VECTOR(n) 列类型 | v24.2 |
CREATE VECTOR INDEX(C-SPANN) | v25.2 |
通过 ttl_expiration_expression 的行级 TTL | v23.1 |
在 CockroachDB v25.2 上,向量索引由一个集群设置把关。在用 CSpannIndex 创建存储之前,先在集群上启用它一次:
sql
SET CLUSTER SETTING feature.vector_index.enabled = true;1
Maven 依赖
NOTE
由于 CockroachDB 支持属于 langchain4j-community,它将从 1.22.0-beta32 或更新版本起可用。
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-cockroachdb</artifactId>
<version>1.22.0-beta32</version>
</dependency>1
2
3
4
5
2
3
4
5
如果你导入 Community BOM,可以省略版本号。
API
该模块暴露四个公共类:
CockroachDbEngine
包装一个 HikariCP DataSource 并处理连接池。它可以从独立的 host/port/database/username/password 字段、从一个完整的连接字符串(Python 风格的 cockroachdb:// scheme 会被自动改写为 jdbc:postgresql://),或经由 CockroachDbEngine.from(dataSource) 从一个已有的 DataSource 构建。
CockroachDbSchema
封装嵌入表布局:表名与列名、向量维度、距离度量、用于多租户的可选命名空间列、所选的向量索引策略,以及一个用于未来混合搜索的可选生成 tsvector 列。
CockroachDbEmbeddingStore
针对原生 CockroachDB VECTOR 列实现了 LangChain4j 的 EmbeddingStore<TextSegment>。支持批量插入、JSONB 元数据过滤、按 id / 按 Filter / 批量移除、可选的命名空间作用域,以及针对 C-SPANN 的可选逐查询 vector_search_beam_size 调优。
CockroachDbChatMemoryStore
实现了 LangChain4j 的 ChatMemoryStore。它把序列化后的聊天消息持久化在一个 JSONB 列中,按一个显式的插入序号排序,并可选带行级 TTL。
连接
CockroachDbEngine 包装一个 HikariDataSource。你可以从一个连接字符串或从独立字段构建它。
java
import dev.langchain4j.community.store.embedding.cockroachdb.CockroachDbEngine;
CockroachDbEngine engine = CockroachDbEngine.builder()
.host("localhost")
.port(26257)
.database("defaultdb")
.username("root")
.password("")
.sslMode("disable")
.build();1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
builder 也接受一个完整的连接字符串。Python 风格的 cockroachdb:// scheme 会被自动改写为 jdbc:postgresql://,因此你可以直接粘贴 Python 库所用的那个 URL:
java
CockroachDbEngine engine = CockroachDbEngine.fromConnectionString(
"cockroachdb://root@localhost:26257/defaultdb?sslmode=disable");1
2
2
如果你已有一个 DataSource,请使用 CockroachDbEngine.from(dataSource)。
向量存储
一个最小的向量存储使用顺序扫描(NoIndex),它适合小数据集与测试:
java
import dev.langchain4j.community.store.embedding.cockroachdb.CockroachDbEmbeddingStore;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.allminilml6v2q.AllMiniLmL6V2QuantizedEmbeddingModel;
EmbeddingModel model = new AllMiniLmL6V2QuantizedEmbeddingModel();
CockroachDbEmbeddingStore store = CockroachDbEmbeddingStore.builder()
.engine(engine)
.dimension(model.dimension())
.tableName("embeddings")
.build();
TextSegment segment = TextSegment.from("Cockroaches are surprisingly resilient.");
Embedding embedding = model.embed(segment).content();
store.add(embedding, segment);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
对于 CockroachDB v25.2+ 上的生产工作负载,请添加一个 C-SPANN 向量索引:
java
import dev.langchain4j.community.store.embedding.cockroachdb.index.CSpannIndex;
CockroachDbEmbeddingStore store = CockroachDbEmbeddingStore.builder()
.engine(engine)
.dimension(model.dimension())
.vectorIndex(CSpannIndex.builder()
.minPartitionSize(16)
.maxPartitionSize(128)
.build())
.build();1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
为该索引发出的 DDL 是:
sql
CREATE VECTOR INDEX IF NOT EXISTS embeddings_embedding_vector_idx
ON public.embeddings (embedding)
WITH (min_partition_size = 16, max_partition_size = 128);1
2
3
2
3
C-SPANN 会从查询运算符挑选距离函数(余弦用 <=>、L2 用 <->、内积用 <#>),因此 MetricType 是在存储上于查询时选定的,而不绑定到索引。
搜索
EmbeddingSearchRequest 的用法与在任何其他 LangChain4j 存储中相同:
java
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
EmbeddingSearchResult<TextSegment> result = store.search(
EmbeddingSearchRequest.builder()
.queryEmbedding(model.embed("resilience").content())
.maxResults(5)
.minScore(0.6)
.build());
result.matches().forEach(m ->
System.out.printf("%s (%.3f) %s%n", m.embeddingId(), m.score(), m.embedded().text()));1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
在查询时调优 C-SPANN
CockroachDB 暴露了一个会话变量 vector_search_beam_size,它控制召回/延迟的权衡。在存储 builder 上设置它,会让每次搜索都被包进一个用 SET LOCAL 限定该设置作用域的事务:
java
CockroachDbEmbeddingStore store = CockroachDbEmbeddingStore.builder()
.engine(engine)
.dimension(model.dimension())
.vectorIndex(CSpannIndex.builder().build())
.searchBeamSize(32)
.build();1
2
3
4
5
6
2
3
4
5
6
更高的值以延迟换取召回。若你把该字段留空,默认束宽由 CockroachDB 决定。
元数据过滤
元数据存储在 JSONB 列中,并在查询时用 LangChain4j 的 Filter 表达式过滤:
java
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder;
EmbeddingSearchResult<TextSegment> result = store.search(
EmbeddingSearchRequest.builder()
.queryEmbedding(query)
.maxResults(10)
.filter(MetadataFilterBuilder.metadataKey("category").isEqualTo("biology")
.and(MetadataFilterBuilder.metadataKey("year").isGreaterThan(2020)))
.build());1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
比较过滤器(>、>=、<、<=)会把 JSONB 值转换为 numeric。字符串的相等比较的是 JSON 文本。过滤键只能包含字母数字字符、点、下划线或连字符。
用命名空间列实现多租户
要按租户限定行的作用域,请在 schema 中加一个 namespaceColumn,并在每个存储实例上配置一个命名空间值。该列会作为前缀加到 C-SPANN 索引上,从而使逐租户的查询保持快速:
java
CockroachDbEmbeddingStore tenantA = CockroachDbEmbeddingStore.builder()
.engine(engine)
.dimension(model.dimension())
.namespaceColumn("tenant_id")
.namespace("acme")
.vectorIndex(CSpannIndex.builder().build())
.build();1
2
3
4
5
6
7
2
3
4
5
6
7
生成的索引变为 CREATE VECTOR INDEX ... ON embeddings (tenant_id, embedding),且经由该存储执行的每次读/写都会被过滤到 tenant_id = 'acme'。
可选的全文列
如果你打算之后把向量搜索与全文搜索结合,请在建表时启用一个生成的 tsvector 列。一个 GIN 索引会随之创建:
java
CockroachDbEmbeddingStore store = CockroachDbEmbeddingStore.builder()
.engine(engine)
.dimension(model.dimension())
.createTsvectorColumn(true)
.build();1
2
3
4
5
2
3
4
5
混合(向量 + FTS)查询执行尚未实现;创建该列是为了它能被应用代码或未来版本使用。
聊天记忆
CockroachDbChatMemoryStore 实现了 ChatMemoryStore,并把序列化后的聊天消息持久化在一个按插入时间排序的 JSONB 列中:
java
import dev.langchain4j.community.store.memory.chat.cockroachdb.CockroachDbChatMemoryStore;
CockroachDbChatMemoryStore memory = CockroachDbChatMemoryStore.builder()
.engine(engine)
.tableName("chat_memory")
.build();1
2
3
4
5
6
2
3
4
5
6
其结构为:
sql
CREATE TABLE chat_memory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id TEXT NOT NULL,
message JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX chat_memory_session_idx ON chat_memory (session_id, created_at);1
2
3
4
5
6
7
2
3
4
5
6
7
updateMessages 在一个事务内替换整个会话,因此部分写入对读者不可见。
行级 TTL
CockroachDB 能自动让行过期。传入一个 ttl 时长,以在聊天记忆表上启用行级 TTL:
java
import java.time.Duration;
CockroachDbChatMemoryStore memory = CockroachDbChatMemoryStore.builder()
.engine(engine)
.tableName("chat_memory")
.ttl(Duration.ofDays(7))
.ttlJobCron("@daily")
.build();1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
结构设置会发出:
sql
ALTER TABLE chat_memory SET (
ttl_expiration_expression = $$(created_at + '7 days')$$,
ttl_job_cron = '@daily'
);1
2
3
4
2
3
4
要在一个已有表上禁用 TTL:
java
memory.disableTtl();1
重试
当某个事务在其默认的 SERIALIZABLE 隔离级别下必须重试时,CockroachDB 会返回 SQLSTATE 40001。该存储把每个工作单元包在一个带指数退避与抖动的重试循环中(默认 5 次尝试,从 100 毫秒起,倍增至上限 10 秒)。无需额外配置。
连接字符串格式
以下形式都能被 CockroachDbEngine.fromConnectionString 接受:
| 形式 | 示例 |
|---|---|
| Python 风格 | cockroachdb://root@localhost:26257/defaultdb?sslmode=disable |
| psycopg 风格 | cockroachdb+psycopg://user:pw@host:26257/db |
| libpq 风格 | postgresql://user@host:26257/db |
| JDBC 风格 | jdbc:postgresql://localhost:26257/defaultdb |
对于 CockroachDB Cloud,请使用集群控制台里的连接字符串,通常是:
cockroachdb://USER:PASSWORD@HOST:26257/DATABASE?sslmode=verify-full1
参数总览
CockroachDbEngine 参数
| 参数 | 描述 | 默认值 | 必填/可选 |
|---|---|---|---|
host | CockroachDB 服务器的主机名 | localhost | 必填(若无 connectionString) |
port | CockroachDB 服务器的端口号 | 26257 | 必填(若无 connectionString) |
database | 要连接的数据库 | defaultdb | 必填(若无 connectionString) |
username | 认证的用户名 | root | 必填 |
password | 认证的密码 | ""(空) | 可选 |
schema | 默认 schema 名 | public | 可选 |
sslMode | SSL 模式(disable、require、verify-full 等) | disable | 可选 |
maxPoolSize | HikariCP 池的最大大小 | 10 | 可选 |
minPoolSize | 最小空闲连接数 | 5 | 可选 |
connectionTimeoutMs | 连接超时(毫秒) | 10000 | 可选 |
idleTimeoutMs | 空闲超时(毫秒) | 300000 | 可选 |
maxLifetimeMs | 连接最大存活时间(毫秒) | 3600000 | 可选 |
connectionString | 完整 URL;设置时会覆盖独立的 host/port/db | null | 可选 |
CockroachDbEmbeddingStore 参数
| 参数 | 描述 | 默认值 | 必填/可选 |
|---|---|---|---|
engine | CockroachDbEngine 实例 | 无 | 必填 |
dimension | 嵌入向量维度 | 无 | 必填 |
tableName | 嵌入表名 | embeddings | 可选 |
schemaName | 数据库 schema 名 | public | 可选 |
metricType | 距离度量:COSINE、EUCLIDEAN 或 DOT_PRODUCT | COSINE | 可选 |
vectorIndex | CSpannIndex 或 NoIndex | NoIndex(顺序扫描) | 可选 |
namespaceColumn | 用于多租户的租户列名 | null(禁用) | 可选 |
namespace | 在每次读与写上应用的租户值 | null | 可选,需要 namespaceColumn |
searchBeamSize | 逐查询的 vector_search_beam_size 会话变量 | null(CockroachDB 默认) | 可选 |
createTableIfNotExists | 在构建时建表 | true | 可选 |
createTsvectorColumn | 添加一个生成的 tsvector 列 + GIN 索引 | false | 可选 |
CSpannIndex 参数(CockroachDB v25.2+)
| 参数 | 描述 | 默认值 | 必填/可选 |
|---|---|---|---|
name | 自定义索引名 | {table}_{column}_vector_idx | 可选 |
minPartitionSize | 最小分区大小(经 WITH 发出) | CockroachDB 默认 | 可选 |
maxPartitionSize | 最大分区大小(经 WITH 发出) | CockroachDB 默认 | 可选 |
CockroachDbChatMemoryStore 参数
| 参数 | 描述 | 默认值 | 必填/可选 |
|---|---|---|---|
engine | CockroachDbEngine 实例 | 无 | 必填 |
tableName | 聊天历史表名 | message_store | 可选 |
schemaName | 数据库 schema 名 | public | 可选 |
ttl | 行级 TTL 时长;设置时在 CockroachDB 上启用 TTL | null(禁用) | 可选 |
ttlJobCron | TTL 任务调度 | @daily | 可选,需要 ttl |
createTableIfNotExists | 在构建时建表 | true | 可选 |
示例
一个最小的端到端 RAG 演示,它会启动一个 CockroachDB Testcontainer、为两个文本片段建索引,并运行一次相似度搜索:
java
import dev.langchain4j.community.store.embedding.cockroachdb.CockroachDbEmbeddingStore;
import dev.langchain4j.community.store.embedding.cockroachdb.CockroachDbEngine;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore;
import java.util.List;
import org.testcontainers.containers.CockroachContainer;
public class CockroachDbEmbeddingStoreExample {
public static void main(String[] args) {
try (CockroachContainer cockroach = new CockroachContainer("cockroachdb/cockroach:latest-v25.2")) {
cockroach.start();
CockroachDbEngine engine = CockroachDbEngine.builder()
.connectionString(cockroach.getJdbcUrl())
.username(cockroach.getUsername())
.password(cockroach.getPassword())
.build();
EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
EmbeddingStore<TextSegment> embeddingStore = CockroachDbEmbeddingStore.builder()
.engine(engine)
.dimension(embeddingModel.dimension())
.tableName("demo_embeddings")
.build();
TextSegment segment1 = TextSegment.from("I like football.");
Embedding embedding1 = embeddingModel.embed(segment1).content();
embeddingStore.add(embedding1, segment1);
TextSegment segment2 = TextSegment.from("The weather is good today.");
Embedding embedding2 = embeddingModel.embed(segment2).content();
embeddingStore.add(embedding2, segment2);
Embedding queryEmbedding = embeddingModel.embed("What is your favourite sport?").content();
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(1)
.build();
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.search(request).matches();
EmbeddingMatch<TextSegment> match = matches.get(0);
System.out.println(match.score()); // ~0.81
System.out.println(match.embedded().text()); // I like football.
engine.close();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
该示例使用默认的默认顺序扫描索引,因此它无需额外集群配置就能在任何 CockroachDB v24.2 或更新版本上运行。要在 v25.2 或更新版本上切换到 C-SPANN 分布式 ANN 索引,请在集群上启用该特性开关一次,并通过 .vectorIndex(...) 向存储传入 CSpannIndex.builder().build():
sql
SET CLUSTER SETTING feature.vector_index.enabled = true;1
一个更完整的可运行版本位于 langchain4j-examples/cockroachdb-example。
已知限制
- C-SPANN 向量索引需要 CockroachDB v25.2 或更新版本,且必须启用
feature.vector_index.enabled集群设置。 - 向量值以文本发送并用
?::vector转换,因为 CockroachDB 的 pgwire 层不接受VECTOR类型的二进制格式。 - 混合(向量 + 全文)查询执行尚未实现。tsvector 列与 GIN 索引可通过
createTsvectorColumn创建,以供应用代码或未来版本使用。 - Python 的
langchain-cockroachdb库还带有一个 LangGraph checkpointer(CockroachDBSaver与AsyncCockroachDBSaver)。其 Java 对应物位于第三方 langgraph4j 项目中,名为langgraph4j-cockroachdb-saver。langgraph4j 的 checkpoint 契约没有异步 API,因此只提供同步的CockroachDBSaver;在 JDK 21 或更新版本上的调用方可以从一个虚拟线程调用它,以获得非阻塞并发。