mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 08:25:00 +00:00
test(rag): 增加三向量库 Docker 生命周期测试
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -53,3 +53,4 @@ logs/
|
|||||||
|
|
||||||
.flattened-pom.xml
|
.flattened-pom.xml
|
||||||
/.claude/settings.local.json
|
/.claude/settings.local.json
|
||||||
|
/docs/docker/milvus/volumes/
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
version: '3.5'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
etcd:
|
etcd:
|
||||||
container_name: milvus-etcd
|
container_name: ruoyi-rag-milvus-etcd
|
||||||
image: quay.io/coreos/etcd:v3.5.18
|
image: quay.io/coreos/etcd:v3.5.18
|
||||||
environment:
|
environment:
|
||||||
- ETCD_AUTO_COMPACTION_MODE=revision
|
- ETCD_AUTO_COMPACTION_MODE=revision
|
||||||
@@ -19,14 +17,11 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
minio:
|
minio:
|
||||||
container_name: milvus-minio
|
container_name: ruoyi-rag-milvus-minio
|
||||||
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
|
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
|
||||||
environment:
|
environment:
|
||||||
MINIO_ACCESS_KEY: minioadmin
|
MINIO_ACCESS_KEY: minioadmin
|
||||||
MINIO_SECRET_KEY: minioadmin
|
MINIO_SECRET_KEY: minioadmin
|
||||||
ports:
|
|
||||||
- "9001:9001"
|
|
||||||
- "9000:9000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
|
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
|
||||||
command: minio server /minio_data --console-address ":9001"
|
command: minio server /minio_data --console-address ":9001"
|
||||||
@@ -37,7 +32,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
standalone:
|
standalone:
|
||||||
container_name: milvus-standalone
|
container_name: ruoyi-rag-milvus
|
||||||
image: milvusdb/milvus:v2.5.7
|
image: milvusdb/milvus:v2.5.7
|
||||||
command: ["milvus", "run", "standalone"]
|
command: ["milvus", "run", "standalone"]
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -61,7 +56,7 @@ services:
|
|||||||
- "minio"
|
- "minio"
|
||||||
|
|
||||||
attu:
|
attu:
|
||||||
container_name: attu
|
container_name: ruoyi-rag-attu
|
||||||
image: zilliz/attu:v2.5.7
|
image: zilliz/attu:v2.5.7
|
||||||
environment:
|
environment:
|
||||||
MILVUS_URL: milvus-standalone:19530
|
MILVUS_URL: milvus-standalone:19530
|
||||||
@@ -72,4 +67,4 @@ services:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
name: milvus
|
name: ruoyi-rag-milvus
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
qdrant:
|
qdrant:
|
||||||
image: qdrant/qdrant:latest
|
container_name: ruoyi-rag-qdrant
|
||||||
|
image: qdrant/qdrant:v1.17.0
|
||||||
ports:
|
ports:
|
||||||
- 6333:6333
|
- 6333:6333
|
||||||
- 6334:6334
|
- 6334:6334
|
||||||
volumes:
|
volumes:
|
||||||
- qdrant_data:/qdrant/storage
|
- qdrant_data:/qdrant/storage
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "bash -c 'exec 3<>/dev/tcp/127.0.0.1/6333 && printf \"GET /healthz HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n\" >&3 && grep -q \"200 OK\" <&3'"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 10s
|
||||||
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
qdrant_data:
|
qdrant_data:
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package org.ruoyi.service.vector.impl;
|
||||||
|
|
||||||
|
import dev.langchain4j.data.embedding.Embedding;
|
||||||
|
import dev.langchain4j.data.segment.TextSegment;
|
||||||
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
|
import dev.langchain4j.model.output.Response;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||||
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
|
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||||
|
import org.ruoyi.config.VectorStoreProperties;
|
||||||
|
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||||
|
import org.ruoyi.domain.bo.vector.StoreEmbeddingBo;
|
||||||
|
import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo;
|
||||||
|
import org.ruoyi.domain.entity.knowledge.KnowledgeInfo;
|
||||||
|
import org.ruoyi.factory.EmbeddingModelFactory;
|
||||||
|
import org.ruoyi.enums.ModalityType;
|
||||||
|
import org.ruoyi.mapper.knowledge.KnowledgeAttachMapper;
|
||||||
|
import org.ruoyi.mapper.knowledge.KnowledgeInfoMapper;
|
||||||
|
import org.ruoyi.service.vector.VectorStoreService;
|
||||||
|
import org.ruoyi.service.embed.BaseEmbedModelService;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@Tag("dev")
|
||||||
|
@EnabledIfEnvironmentVariable(named = "RAG_DOCKER_IT", matches = "true")
|
||||||
|
class ThreeVectorStoresDockerIT {
|
||||||
|
|
||||||
|
private static final String MODEL = "codex-deterministic-embedding";
|
||||||
|
private static final int DIMENSION = 32;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void weaviateLifecycle() {
|
||||||
|
verifyLifecycle("weaviate", strategy("weaviate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void milvusLifecycle() {
|
||||||
|
verifyLifecycle("milvus", strategy("milvus"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void qdrantLifecycle() {
|
||||||
|
verifyLifecycle("qdrant", strategy("qdrant"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyLifecycle(String type, VectorStoreService strategy) {
|
||||||
|
String kid = switch (type) {
|
||||||
|
case "weaviate" -> "990000001";
|
||||||
|
case "milvus" -> "990000002";
|
||||||
|
case "qdrant" -> "990000003";
|
||||||
|
default -> throw new IllegalArgumentException(type);
|
||||||
|
};
|
||||||
|
String docId = "codex_rag_verify_doc_" + type;
|
||||||
|
List<String> fids = List.of(
|
||||||
|
"codex_rag_verify_fid_" + type + "_1",
|
||||||
|
"codex_rag_verify_fid_" + type + "_2");
|
||||||
|
try {
|
||||||
|
strategy.createSchema(kid, MODEL);
|
||||||
|
strategy.storeEmbeddings(store(type, kid, docId, fids));
|
||||||
|
|
||||||
|
List<KnowledgeRetrievalVo> found = strategy.search(query(type, kid, "deterministic alpha"));
|
||||||
|
assertFalse(found.isEmpty(), type + " must return stored vectors");
|
||||||
|
assertTrue(found.stream().anyMatch(v -> fids.contains(v.getId())));
|
||||||
|
|
||||||
|
strategy.removeByFid(fids.get(0), kid);
|
||||||
|
assertEventually(() -> strategy.search(query(type, kid, "deterministic alpha")).stream()
|
||||||
|
.noneMatch(v -> fids.get(0).equals(v.getId())), type + " fid delete");
|
||||||
|
|
||||||
|
strategy.removeByDocId(docId, kid);
|
||||||
|
assertEventually(() -> strategy.search(query(type, kid, "deterministic beta")).isEmpty(),
|
||||||
|
type + " doc delete");
|
||||||
|
} finally {
|
||||||
|
strategy.removeById(kid, MODEL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertEventually(BooleanSupplier condition, String operation) {
|
||||||
|
long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(15);
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
|
if (condition.getAsBoolean()) return;
|
||||||
|
try {
|
||||||
|
Thread.sleep(250);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
fail(operation + " interrupted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fail(operation + " did not become visible within 15 seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
private VectorStoreService strategy(String type) {
|
||||||
|
VectorStoreProperties properties = properties();
|
||||||
|
EmbeddingModelFactory factory = mock(EmbeddingModelFactory.class);
|
||||||
|
when(factory.createModel(MODEL)).thenReturn(new DeterministicEmbeddingModel());
|
||||||
|
IChatModelService modelService = mock(IChatModelService.class);
|
||||||
|
ChatModelVo model = new ChatModelVo();
|
||||||
|
model.setModelDimension(DIMENSION);
|
||||||
|
when(modelService.selectModelByName(MODEL)).thenReturn(model);
|
||||||
|
KnowledgeAttachMapper attachMapper = mock(KnowledgeAttachMapper.class);
|
||||||
|
return switch (type) {
|
||||||
|
case "weaviate" -> new WeaviateVectorStoreStrategy(properties, modelService, factory, attachMapper);
|
||||||
|
case "milvus" -> {
|
||||||
|
KnowledgeInfoMapper infoMapper = mock(KnowledgeInfoMapper.class);
|
||||||
|
KnowledgeInfo info = new KnowledgeInfo();
|
||||||
|
info.setEmbeddingModel(MODEL);
|
||||||
|
when(infoMapper.selectById(anyLong())).thenReturn(info);
|
||||||
|
yield new MilvusVectorStoreStrategy(properties, modelService, factory, attachMapper, infoMapper);
|
||||||
|
}
|
||||||
|
case "qdrant" -> new QdrantVectorStoreStrategy(properties, modelService, factory, attachMapper);
|
||||||
|
default -> throw new IllegalArgumentException(type);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private VectorStoreProperties properties() {
|
||||||
|
VectorStoreProperties p = new VectorStoreProperties();
|
||||||
|
p.getWeaviate().setProtocol("http");
|
||||||
|
p.getWeaviate().setHost("127.0.0.1:28080");
|
||||||
|
p.getWeaviate().setClassname("CodexRagVerify");
|
||||||
|
p.getMilvus().setUrl("http://127.0.0.1:19530");
|
||||||
|
p.getMilvus().setCollectionname("CodexRagVerify");
|
||||||
|
p.getQdrant().setHost("127.0.0.1");
|
||||||
|
p.getQdrant().setPort(6334);
|
||||||
|
p.getQdrant().setCollectionname("CodexRagVerify");
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StoreEmbeddingBo store(String type, String kid, String docId, List<String> fids) {
|
||||||
|
StoreEmbeddingBo bo = new StoreEmbeddingBo();
|
||||||
|
bo.setVectorStoreName(type);
|
||||||
|
bo.setKid(kid);
|
||||||
|
bo.setDocId(docId);
|
||||||
|
bo.setEmbeddingModelName(MODEL);
|
||||||
|
bo.setFids(fids);
|
||||||
|
bo.setChunkList(List.of("deterministic alpha", "deterministic beta"));
|
||||||
|
return bo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryVectorBo query(String type, String kid, String text) {
|
||||||
|
QueryVectorBo bo = new QueryVectorBo();
|
||||||
|
bo.setVectorModelName(type);
|
||||||
|
bo.setKid(kid);
|
||||||
|
bo.setQuery(text);
|
||||||
|
bo.setEmbeddingModelName(MODEL);
|
||||||
|
bo.setMaxResults(10);
|
||||||
|
return bo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DeterministicEmbeddingModel implements BaseEmbedModelService {
|
||||||
|
@Override
|
||||||
|
public void configure(ChatModelVo config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<ModalityType> getSupportedModalities() {
|
||||||
|
return Set.of(ModalityType.TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<Embedding> embed(String text) {
|
||||||
|
return Response.from(vector(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<Embedding> embed(TextSegment segment) {
|
||||||
|
return embed(segment.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<List<Embedding>> embedAll(List<TextSegment> segments) {
|
||||||
|
List<Embedding> embeddings = new ArrayList<>(segments.size());
|
||||||
|
for (TextSegment segment : segments) embeddings.add(vector(segment.text()));
|
||||||
|
return Response.from(embeddings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Embedding vector(String text) {
|
||||||
|
try {
|
||||||
|
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(text.getBytes(StandardCharsets.UTF_8));
|
||||||
|
float[] values = new float[DIMENSION];
|
||||||
|
for (int i = 0; i < values.length; i++) values[i] = (digest[i] & 0xff) / 255.0f;
|
||||||
|
return Embedding.from(values);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user