共计 2430 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
在处理大模型应用中的向量数据时,传统关系型数据库显得力不从心。以推荐系统为例,当需要实时计算用户偏好与百万级商品向量的相似度时,MySQL 等数据库面临三个核心问题:

- 计算效率低下 :关系型数据库的 B 树索引对 768 维向量的最近邻搜索完全失效,全表扫描导致响应时间超过 2 秒
- 存储成本高 :每个向量需要拆分成多行存储,1 亿条向量数据占用空间超过 500GB
- 扩展性差 :垂直扩容无法应对突发流量,分库分表方案破坏向量搜索的完整性
技术选型
对比主流向量数据库在 SpringBoot 环境的表现(测试环境:4 核 8G 云服务器,100 万条 768 维向量):
| 方案 | 集成复杂度 | 内存占用 | QPS(TOP10 搜索) | 学习成本 |
|---|---|---|---|---|
| ChromaDB | ★★☆ | 2.1GB | 850 | 低 |
| Milvus | ★★★☆ | 5.8GB | 1200 | 高 |
| Pinecone | ★★☆ | 云托管 | 700 | 中 |
ChromaDB 胜出的关键点:
- 内置 Python/HTTP 接口,与 Java 生态友好
- 基于 SQLite 的轻量级设计,适合中小规模场景
- 无需维护独立集群,降低运维成本
核心实现
1. 自定义 Repository
@Repository
public interface ProductVectorRepository extends CrudRepository<Product, String> {@Query("SELECT id FROM products ORDER BY embedding <=> ?1 LIMIT ?2")
List<String> findSimilarIds(float[] embedding, int limit);
}
2. 向量索引构建
@Data
@Embeddable
public class ProductEmbedding {@Embedding(dimension = 768)
private float[] vector;
@PrePersist
void normalize() {
// 归一化处理提升搜索精度
float norm = (float) Math.sqrt(Arrays.stream(vector).map(x -> x * x).sum());
for (int i = 0; i < vector.length; i++) {vector[i] /= norm;
}
}
}
批量插入优化技巧:
- 使用 Spring Batch 的 ItemWriter 实现分块写入
- 开启 SQLite 的 WAL 模式提升并发
- 采用并行流处理 CPU 密集型计算
3. 相似度搜索服务
@Slf4j
@Service
@RequiredArgsConstructor
public class VectorSearchService {
private final ProductVectorRepository repository;
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
@SneakyThrows
public CompletableFuture<List<Product>> searchSimilar(float[] queryVector, int topK) {return CompletableFuture.supplyAsync(() -> {long start = System.nanoTime();
List<String> ids = repository.findSimilarIds(queryVector, topK);
log.debug("Search latency: {}ms", (System.nanoTime()-start)/1e6);
return repository.findAllById(ids);
}, executor);
}
}
生产考量
内存管理
在 application.yaml 中配置:
chromadb:
max_connection_memory: 2GB
jna:
library_path: /usr/lib/x86_64-linux-gnu/libsqlite3.so
JVM 参数建议:
-XX:MaxDirectMemorySize=4G
-XX:+UseZGC
-XX:NativeMemoryTracking=detail
并发优化
关键参数设置:
- gRPC 工作线程数 = CPU 核心数 * 2
- SQLite 连接池大小 = 50(避免锁竞争)
- 启用 HTTP/ 2 多路复用
监控集成
@Configuration
public class MetricsConfig {
@Bean
MeterBinder chromaMetrics(ChromadbClient client) {
return registry -> {Gauge.builder("chroma.collections", client::getCollectionCount)
.register(registry);
Timer.builder("chroma.query.time")
.publishPercentiles(0.5, 0.95)
.register(registry);
};
}
}
避坑指南
维度校验
public void validateDimension(float[] input) {if (input.length != 768) {
throw new InvalidDimensionException("Expected 768 dimensions, got" + input.length);
}
}
冷启动优化
- 启动时加载高频查询的向量到 Caffeine 缓存
- 使用 MMap 加速磁盘数据读取
- 预热线程池避免首次请求延迟
分片策略
当数据超过 500 万条时建议:
- 按业务 ID 范围分片(如用户 ID 哈希)
- 每个分片不超过 100GB
- 查询路由层聚合各分片结果
思考题
如何设计混合检索(向量 + 标量)的联合查询?例如同时满足:
- 商品向量与用户偏好相似度 > 0.8
- 价格区间在 100-500 元
- 月销量超过 1000
可能的实现方向:
- 两阶段过滤:先标量后向量
- 构建组合索引
- 自定义评分函数
欢迎在评论区分享你的方案。
正文完
发表至: 技术分享
近一天内
