diff --git a/VectorGraphRag/pom.xml b/VectorGraphRag/pom.xml new file mode 100644 index 0000000..c28d0b1 --- /dev/null +++ b/VectorGraphRag/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + com.zjkl + vector-graph-rag + 0.1.0 + jar + Vector Graph RAG + Graph RAG using pure vector search with Milvus + + + 21 + 21 + 21 + UTF-8 + 3.5.11 + 2.6.18 + 2.13.2 + 3.2.0 + 1.18.44 + 2.0.17 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + io.milvus + milvus-sdk-java + ${milvus-sdk.version} + + + + com.google.code.gson + gson + ${gson.version} + + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + 21 + 21 + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/VectorGraphRAG.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/VectorGraphRAG.java new file mode 100644 index 0000000..8cba48a --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/VectorGraphRAG.java @@ -0,0 +1,451 @@ +package com.zjkl.vectorgraphrag; + +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import com.zjkl.vectorgraphrag.graph.Graph; +import com.zjkl.vectorgraphrag.graph.GraphBuilder; +import com.zjkl.vectorgraphrag.graph.GraphRetriever; +import com.zjkl.vectorgraphrag.graph.SubGraph; +import com.zjkl.vectorgraphrag.llm.AnswerGenerator; +import com.zjkl.vectorgraphrag.llm.EntityExtractor; +import com.zjkl.vectorgraphrag.llm.LLMCache; +import com.zjkl.vectorgraphrag.llm.LLMReranker; +import com.zjkl.vectorgraphrag.llm.OpenAiClient; +import com.zjkl.vectorgraphrag.llm.TripletExtractor; +import com.zjkl.vectorgraphrag.model.*; +import com.zjkl.vectorgraphrag.storage.EmbeddingClient; +import com.zjkl.vectorgraphrag.storage.MilvusStore; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Vector Graph RAG - Graph RAG using pure vector search with Milvus. + * + * Main entry point for building and querying a Graph RAG system. + * Implements the full pipeline: triplet extraction → graph building → + * embedding → Milvus indexing → multi-way retrieval → subgraph expansion → + * LLM reranking → answer generation. + * + * Example: + *
{@code
+ * VectorGraphRagSettings settings = VectorGraphRagSettings.builder()
+ *     .openaiApiKey(System.getenv("OPENAI_API_KEY"))
+ *     .milvusUri("./my_graph.db")
+ *     .build();
+ *
+ * VectorGraphRAG rag = new VectorGraphRAG(settings);
+ *
+ * rag.addTexts(List.of(
+ *     "Albert Einstein developed the theory of relativity.",
+ *     "The theory of relativity revolutionized physics."
+ * ));
+ *
+ * QueryResult result = rag.query("What did Einstein develop?");
+ * System.out.println(result.getAnswer());
+ * }
+ */ +@Slf4j +public class VectorGraphRAG { + + private final VectorGraphRagSettings settings; + + private final EmbeddingClient embeddingClient; + private final MilvusStore store; + private final GraphBuilder graphBuilder; + private final TripletExtractor tripletExtractor; + private final EntityExtractor entityExtractor; + private final LLMReranker reranker; + private final AnswerGenerator answerGenerator; + private final LLMCache llmCache; + private final OpenAiClient openAiClient; + private final Graph graph; + + @Getter + private GraphRetriever retriever; + + @Getter + private ExtractionResult extractionResult; + + /** + * Create VectorGraphRAG with custom settings. + */ + public VectorGraphRAG(VectorGraphRagSettings settings) { + this.settings = settings; + this.llmCache = settings.isUseLlmCache() ? new LLMCache() : null; + this.openAiClient = new OpenAiClient(settings, llmCache); + this.embeddingClient = new EmbeddingClient(settings, openAiClient); + this.store = new MilvusStore(settings, embeddingClient); + this.graphBuilder = new GraphBuilder(settings); + this.tripletExtractor = new TripletExtractor(settings, openAiClient); + this.entityExtractor = new EntityExtractor(settings, openAiClient); + this.reranker = new LLMReranker(settings, openAiClient); + this.answerGenerator = new AnswerGenerator(settings, openAiClient); + this.graph = new Graph(settings, store, embeddingClient); + + // Create collections if they don't exist + store.createCollections(false); + } + + /** + * Create VectorGraphRAG with default settings (reads OPENAI_API_KEY from environment). + */ + public static VectorGraphRAG createDefault() { + VectorGraphRagSettings settings = VectorGraphRagSettings.builder() + .openaiApiKey(System.getenv().getOrDefault("OPENAI_API_KEY", "")) + .build(); + return new VectorGraphRAG(settings); + } + + // ==================== Indexing ==================== + + /** + * Add text strings to the knowledge base. + */ + public ExtractionResult addTexts(List texts) { + return addTexts(texts, null, true); + } + + /** + * Add text strings with extraction control. + */ + public ExtractionResult addTexts(List texts, List ids, boolean extractTriplets) { + List documents = new ArrayList<>(); + for (int i = 0; i < texts.size(); i++) { + String docId = (ids != null && i < ids.size()) ? ids.get(i) : UUID.randomUUID().toString(); + documents.add(Document.builder() + .id(docId) + .text(texts.get(i)) + .build()); + } + return addDocuments(documents, extractTriplets, true); + } + + /** + * Add Document objects to the knowledge base. + * Full indexing pipeline: extract triplets → build graph → embed → index in Milvus. + */ + public ExtractionResult addDocuments(List documents, boolean extractTriplets, boolean showProgress) { + // Ensure all documents have IDs + for (Document doc : documents) { + if (doc.getId() == null) { + doc.setId(UUID.randomUUID().toString()); + } + } + + // Extract triplets + if (extractTriplets) { + documents = tripletExtractor.extractFromDocuments(documents, showProgress); + } + + // Build graph + this.extractionResult = graphBuilder.buildFromDocuments(documents); + + // Generate embeddings + if (showProgress) log.info("Generating embeddings..."); + + List entityTexts = graphBuilder.getEntityTexts(); + List relationTexts = graphBuilder.getRelationTexts(); + List passageTexts = graphBuilder.getPassageTexts(); + + List> entityEmbeddings = entityTexts.isEmpty() ? List.of() + : embeddingClient.embedBatch(entityTexts, showProgress); + List> relationEmbeddings = relationTexts.isEmpty() ? List.of() + : embeddingClient.embedBatch(relationTexts, showProgress); + List> passageEmbeddings = passageTexts.isEmpty() ? List.of() + : embeddingClient.embedBatch(passageTexts, showProgress); + + // Build metadata for adjacency + List> entityMetadatas = new ArrayList<>(); + for (String eid : graphBuilder.getEntityIds()) { + Map meta = new HashMap<>(); + meta.put("relation_ids", graphBuilder.getEntityToRelationIds() + .getOrDefault(eid, List.of())); + meta.put("passage_ids", graphBuilder.getEntityToPassageIds() + .getOrDefault(eid, List.of())); + entityMetadatas.add(meta); + } + + List> relationMetadatas = new ArrayList<>(); + for (String rid : graphBuilder.getRelationIds()) { + Map meta = new HashMap<>(); + meta.put("entity_ids", graphBuilder.getRelationToEntityIds() + .getOrDefault(rid, List.of())); + meta.put("passage_ids", graphBuilder.getRelationToPassageIds() + .getOrDefault(rid, List.of())); + + Triplet triplet = graphBuilder.getRelationIdToTriplet().get(rid); + if (triplet != null) { + meta.put("subject", triplet.getSubject()); + meta.put("predicate", triplet.getPredicate()); + meta.put("object", triplet.getObject()); + } + relationMetadatas.add(meta); + } + + List> passageMetadatas = new ArrayList<>(); + for (String pid : graphBuilder.getPassageIds()) { + Map meta = new HashMap<>(); + meta.put("entity_ids", graphBuilder.getPassageToEntityIds() + .getOrDefault(pid, List.of())); + meta.put("relation_ids", graphBuilder.getPassageToRelationIds() + .getOrDefault(pid, List.of())); + passageMetadatas.add(meta); + } + + // Drop and recreate for idempotent indexing (matching Python reference behavior) + store.dropCollections(); + store.createCollections(true); + if (showProgress) log.info("Inserting into Milvus..."); + + store.insertEntities(entityTexts, graphBuilder.getEntityIds(), + entityEmbeddings, entityMetadatas, showProgress); + store.insertRelations(relationTexts, graphBuilder.getRelationIds(), + relationEmbeddings, relationMetadatas, showProgress); + store.insertPassages(passageTexts, graphBuilder.getPassageIds(), + passageEmbeddings, passageMetadatas, showProgress); + + // Reset retriever + this.retriever = null; + + return extractionResult; + } + + /** + * Add documents with pre-extracted triplets (skip LLM extraction). + */ + public ExtractionResult addDocumentsWithTriplets(List> documents) { + List docs = new ArrayList<>(); + for (Map docData : documents) { + String passage = (String) docData.getOrDefault("passage", docData.get("text")); + if (passage == null) { + throw new IllegalArgumentException("Each document must include 'passage' or 'text'"); + } + + String docId = (String) docData.getOrDefault("id", UUID.randomUUID().toString()); + + @SuppressWarnings("unchecked") + List> rawTriplets = (List>) docData.get("triplets"); + List triplets = new ArrayList<>(); + if (rawTriplets != null) { + for (List raw : rawTriplets) { + if (raw.size() >= 3) { + triplets.add(new Triplet(raw.get(0), raw.get(1), raw.get(2))); + } + } + } + + Document doc = Document.builder() + .id(docId) + .text(passage) + .triplets(triplets) + .build(); + // Store triplets in metadata for graph builder + List> metadataTriplets = triplets.stream() + .map(t -> List.of(t.getSubject(), t.getPredicate(), t.getObject())) + .collect(Collectors.toList()); + doc.getMetadata().put("triplets", metadataTriplets); + + docs.add(doc); + } + + return addDocuments(docs, false, true); + } + + // ==================== Query ==================== + + /** + * Full Graph RAG query pipeline. + */ + public QueryResult query(String question) { + return query(question, true, null, null, null, null, null); + } + + /** + * Query with custom parameters. + */ + public QueryResult query(String question, boolean useReranking, + Integer entityTopK, Integer relationTopK, + Float entitySimilarityThreshold, Float relationSimilarityThreshold, + String filter) { + GraphRetriever ret = getOrCreateRetriever(); + + RetrievalResult retrievalResult = ret.retrieve(question, entityTopK, relationTopK, + entitySimilarityThreshold, relationSimilarityThreshold, + null, filter); + + // Build retrieval detail + RetrievalDetail retrievalDetail = RetrievalDetail.builder() + .entityIds(retrievalResult.getEntityIds()) + .entityTexts(retrievalResult.getEntityTexts()) + .entityScores(retrievalResult.getEntityScores()) + .relationIds(retrievalResult.getRelationIds()) + .relationTexts(retrievalResult.getRelationTexts()) + .relationScores(retrievalResult.getRelationScores()) + .build(); + + // Get candidate relations from subgraph + List candidateIds = retrievalResult.getExpandedRelationIds(); + List candidateTexts = retrievalResult.getExpandedRelationTexts(); + + // Rerank + RerankResult rerankResult = null; + List rerankedIds; + List rerankedTexts; + + if (useReranking && !candidateIds.isEmpty()) { + Map.Entry, List> result = reranker.rerank( + question, candidateIds, candidateTexts); + rerankedIds = result.getKey(); + rerankedTexts = result.getValue(); + rerankResult = RerankResult.builder() + .selectedRelationIds(rerankedIds) + .selectedRelationTexts(rerankedTexts) + .build(); + } else { + rerankedIds = candidateIds.size() > settings.getFinalTopK() + ? candidateIds.subList(0, settings.getFinalTopK()) + : candidateIds; + rerankedTexts = candidateTexts.size() > settings.getFinalTopK() + ? candidateTexts.subList(0, settings.getFinalTopK()) + : candidateTexts; + } + + // Get passages from reranked relations + List finalPassages = getPassagesFromRelations(rerankedIds, filter); + + // Hybrid fallback: if graph retrieval yields fewer passages than needed, + // supplement with naive vector search on passages directly + if (finalPassages.size() < settings.getFinalTopK()) { + int needed = settings.getFinalTopK() - finalPassages.size(); + Set existing = new HashSet<>(finalPassages); + List naivePassages = ret.retrievePassagesNaive(question, needed, filter); + for (String p : naivePassages) { + if (existing.add(p)) { + finalPassages.add(p); + if (finalPassages.size() >= settings.getFinalTopK()) break; + } + } + } + + if (finalPassages.size() > settings.getFinalTopK()) { + finalPassages = finalPassages.subList(0, settings.getFinalTopK()); + } + + // Generate answer + String answer = answerGenerator.generate(question, finalPassages); + + return QueryResult.builder() + .query(question) + .answer(answer) + .queryEntities(retrievalResult.getQueryEntities()) + .retrievedPassages(finalPassages) + .retrievedRelations(retrievalResult.getRelationTexts()) + .expandedRelations(candidateTexts) + .rerankedRelations(rerankedTexts) + .subgraph(retrievalResult.getSubgraph()) + .passages(finalPassages) + .retrievalDetail(retrievalDetail) + .rerankResult(rerankResult) + .evictionResult(EvictionResult.builder() + .occurred(retrievalResult.isEvictionOccurred()) + .beforeCount(retrievalResult.getEvictionBeforeCount()) + .afterCount(retrievalResult.getEvictionAfterCount()) + .build()) + .build(); + } + + /** + * Simple query that returns just the answer string. + */ + public String querySimple(String question) { + return query(question).getAnswer(); + } + + /** + * Naive RAG query (direct passage retrieval, no graph). + */ + public QueryResult queryNaive(String question, String filter) { + GraphRetriever ret = getOrCreateRetriever(); + List passages = ret.retrievePassagesNaive(question, settings.getFinalTopK(), filter); + String answer = answerGenerator.generate(question, passages); + + return QueryResult.builder() + .query(question) + .answer(answer) + .retrievedPassages(passages) + .build(); + } + + // ==================== Utility ==================== + + /** + * Get passages associated with given relation IDs. + */ + private List getPassagesFromRelations(List relationIds, String filter) { + if (relationIds == null || relationIds.isEmpty()) return List.of(); + + List> relationData = store.getRelationsByIds(relationIds); + Set passageIds = new LinkedHashSet<>(); + for (Map rel : relationData) { + @SuppressWarnings("unchecked") + List pids = (List) rel.get("passage_ids"); + if (pids != null) passageIds.addAll(pids); + } + + if (passageIds.isEmpty()) return List.of(); + + List> passageData = store.getPassagesByIds(new ArrayList<>(passageIds), filter); + Map idToText = new LinkedHashMap<>(); + for (Map p : passageData) { + idToText.put(p.get("id").toString(), p.get("text").toString()); + } + + return passageIds.stream() + .map(idToText::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Get knowledge base statistics. + */ + public Map getStats() { + if (extractionResult == null) { + Map stats = new HashMap<>(); + stats.put("entities", 0); + stats.put("relations", 0); + stats.put("passages", 0); + return stats; + } + Map stats = new HashMap<>(); + stats.put("entities", extractionResult.getEntities().size()); + stats.put("relations", extractionResult.getRelations().size()); + stats.put("passages", extractionResult.getDocuments().size()); + return stats; + } + + /** + * Reset the knowledge base, removing all data. + */ + public void reset() { + store.dropCollections(); + store.createCollections(true); + this.extractionResult = null; + this.retriever = null; + } + + private GraphRetriever getOrCreateRetriever() { + if (retriever == null) { + retriever = new GraphRetriever(settings, store, embeddingClient, entityExtractor); + } + return retriever; + } + + // ==================== Accessors ==================== + + public Graph getGraph() { return graph; } + public MilvusStore getStore() { return store; } + public EmbeddingClient getEmbeddingClient() { return embeddingClient; } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings.java new file mode 100644 index 0000000..170a334 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings.java @@ -0,0 +1,110 @@ +package com.zjkl.vectorgraphrag.config; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Component +public class VectorGraphRagSettings { + // OpenAI Settings + @Builder.Default + private String openaiApiKey = System.getenv().getOrDefault("OPENAI_API_KEY", ""); + + @Builder.Default + private String openaiBaseUrl = ""; + + // Model Settings + @Builder.Default + private String llmModel = "gpt-4o-mini"; + + @Builder.Default + private String embeddingModel = "text-embedding-3-large"; + + @Builder.Default + private int embeddingDimension = 3072; + + // Milvus Settings + @Builder.Default + private String milvusUri = "./vector_graph_rag.db"; + + @Builder.Default + private String milvusToken = ""; + + @Builder.Default + private String milvusDb = ""; + + // Milvus Index Settings + @Builder.Default + private String milvusIndexType = "AUTOINDEX"; + + @Builder.Default + private String milvusMetricType = "IP"; + + private Map milvusIndexParams; + + @Builder.Default + private String milvusConsistencyLevel = "Bounded"; + + // Collection prefix + @Builder.Default + private String collectionPrefix = ""; + + // Collection Names + @Builder.Default + private String entityCollection = "vgrag_entities"; + + @Builder.Default + private String relationCollection = "vgrag_relations"; + + @Builder.Default + private String passageCollection = "vgrag_passages"; + + // Retrieval Settings + @Builder.Default + private int entityTopK = 20; + + @Builder.Default + private int relationTopK = 20; + + @Builder.Default + private float entitySimilarityThreshold = 0.9f; + + @Builder.Default + private float relationSimilarityThreshold = -1.0f; + + @Builder.Default + private int expansionDegree = 1; + + @Builder.Default + private int relationNumberThreshold = 1000; + + @Builder.Default + private int finalTopK = 3; + + // LLM Settings + @Builder.Default + private double llmTemperature = 0.0; + + @Builder.Default + private int llmMaxRetries = 3; + + @Builder.Default + private boolean useLlmCache = true; + + // Processing Settings + @Builder.Default + private int batchSize = 32; + + public boolean hasCollectionPrefix() { + return collectionPrefix != null && !collectionPrefix.isEmpty(); + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/Graph.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/Graph.java new file mode 100644 index 0000000..477b63c --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/Graph.java @@ -0,0 +1,258 @@ +package com.zjkl.vectorgraphrag.graph; + +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import com.zjkl.vectorgraphrag.model.*; +import com.zjkl.vectorgraphrag.storage.EmbeddingClient; +import com.zjkl.vectorgraphrag.storage.MilvusStore; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.zjkl.vectorgraphrag.graph.GraphBuilder.normalizePhrase; + +/** + * User-facing graph operations interface. + * Encapsulates MilvusStore with CRUD operations and automatic entity/relation linking. + */ +@Slf4j +public class Graph { + + private final VectorGraphRagSettings settings; + private final EmbeddingClient embeddingClient; + private final MilvusStore store; + + private final Map entityNameToId = new HashMap<>(); + private final Map relationTextToId = new HashMap<>(); + + public Graph(VectorGraphRagSettings settings, MilvusStore store, EmbeddingClient embeddingClient) { + this.settings = settings; + this.store = store; + this.embeddingClient = embeddingClient; + } + + // ==================== Passage CRUD ==================== + + public String createPassage(String text) { + return createPassage(text, null, null); + } + + public String createPassage(String text, String id) { + return createPassage(text, id, null); + } + + public String createPassage(String text, String id, List triplets) { + String passageId = id != null ? id : UUID.randomUUID().toString(); + List embedding = embeddingClient.embed(text); + + List entityIds = new ArrayList<>(); + List relationIds = new ArrayList<>(); + + if (triplets != null) { + for (Triplet triplet : triplets) { + String relationId = createRelation(triplet.getSubject(), triplet.getPredicate(), + triplet.getObject(), null, List.of(passageId)); + relationIds.add(relationId); + + String subjectId = entityNameToId.get(normalizePhrase(triplet.getSubject())); + String objectId = entityNameToId.get(normalizePhrase(triplet.getObject())); + if (subjectId != null && !entityIds.contains(subjectId)) entityIds.add(subjectId); + if (objectId != null && !entityIds.contains(objectId)) entityIds.add(objectId); + } + } + + Map metadata = new HashMap<>(); + if (!entityIds.isEmpty()) metadata.put("entity_ids", entityIds); + if (!relationIds.isEmpty()) metadata.put("relation_ids", relationIds); + + List> metadatas = metadata.isEmpty() ? null : List.of(metadata); + store.insertPassages(List.of(text), List.of(passageId), List.of(embedding), metadatas, false); + + return passageId; + } + + public Passage getPassage(String passageId) { + List> results = store.getPassagesByIds(List.of(passageId)); + if (results.isEmpty()) return null; + + Map data = results.get(0); + return Passage.builder() + .id(safeGet(data, "id")) + .text(safeGet(data, "text")) + .entityIds(safeGetList(data, "entity_ids")) + .relationIds(safeGetList(data, "relation_ids")) + .build(); + } + + public List searchPassages(String query, int topK) { + List queryEmbedding = embeddingClient.embed(query); + List> results = store.searchPassages(queryEmbedding, topK, null); + + return results.stream().map(r -> { + @SuppressWarnings("unchecked") + Map entity = (Map) r.get("entity"); + return Passage.builder() + .id(safeGet(entity, "id")) + .text(safeGet(entity, "text")) + .entityIds(safeGetList(entity, "entity_ids")) + .relationIds(safeGetList(entity, "relation_ids")) + .build(); + }).collect(Collectors.toList()); + } + + public boolean updatePassage(String passageId, String text, List entityIds, List relationIds) { + return store.upsertPassage(passageId, text, null, entityIds, relationIds); + } + + public boolean deletePassage(String passageId) { + List> passages = store.getPassagesByIds(List.of(passageId)); + if (passages.isEmpty()) return false; + + Map data = passages.get(0); + List entityIds = safeGetList(data, "entity_ids"); + List relationIds = safeGetList(data, "relation_ids"); + + // Cascade: remove passage reference from entities + for (String eid : entityIds) { + List> entities = store.getEntitiesByIds(List.of(eid)); + if (!entities.isEmpty()) { + List pids = safeGetList(entities.get(0), "passage_ids"); + pids.remove(passageId); + store.upsertEntity(eid, null, null, null, pids); + } + } + + // Cascade: remove passage reference from relations + for (String rid : relationIds) { + List> rels = store.getRelationsByIds(List.of(rid)); + if (!rels.isEmpty()) { + List pids = safeGetList(rels.get(0), "passage_ids"); + pids.remove(passageId); + store.upsertRelation(rid, null, null, null, pids, null, null, null); + } + } + + return store.deletePassage(passageId); + } + + // ==================== Private: Entity & Relation CRUD ==================== + + private String createRelation(String subject, String predicate, String object, + String id, List passageIds) { + String normSubject = normalizePhrase(subject); + String normPredicate = normalizePhrase(predicate); + String normObject = normalizePhrase(object); + String relationText = normSubject + " " + normPredicate + " " + normObject; + + if (relationTextToId.containsKey(relationText)) { + String existingId = relationTextToId.get(relationText); + if (passageIds != null && !passageIds.isEmpty()) { + List> existing = store.getRelationsByIds(List.of(existingId)); + if (!existing.isEmpty()) { + List currentPids = safeGetList(existing.get(0), "passage_ids"); + Set merged = new LinkedHashSet<>(currentPids); + merged.addAll(passageIds); + store.upsertRelation(existingId, null, null, null, new ArrayList<>(merged), + null, null, null); + } + } + return existingId; + } + + String relationId = id != null ? id : UUID.randomUUID().toString(); + + String subjectId = createEntity(subject, List.of(relationId), passageIds); + String objectId = createEntity(object, List.of(relationId), passageIds); + + List embedding = embeddingClient.embed(relationText); + + Map metadata = new HashMap<>(); + metadata.put("entity_ids", List.of(subjectId, objectId)); + metadata.put("subject", normSubject); + metadata.put("predicate", normPredicate); + metadata.put("object", normObject); + if (passageIds != null && !passageIds.isEmpty()) { + metadata.put("passage_ids", passageIds); + } + + store.insertRelations(List.of(relationText), List.of(relationId), + List.of(embedding), List.of(metadata), false); + + relationTextToId.put(relationText, relationId); + return relationId; + } + + private String createEntity(String name, List relationIds, List passageIds) { + String normalized = normalizePhrase(name); + if (entityNameToId.containsKey(normalized)) { + String existingId = entityNameToId.get(normalized); + if (relationIds != null || passageIds != null) { + List> existing = store.getEntitiesByIds(List.of(existingId)); + if (!existing.isEmpty()) { + List currentRels = safeGetList(existing.get(0), "relation_ids"); + List currentPids = safeGetList(existing.get(0), "passage_ids"); + Set mergedRels = new LinkedHashSet<>(currentRels); + if (relationIds != null) mergedRels.addAll(relationIds); + Set mergedPids = new LinkedHashSet<>(currentPids); + if (passageIds != null) mergedPids.addAll(passageIds); + store.upsertEntity(existingId, null, null, + new ArrayList<>(mergedRels), new ArrayList<>(mergedPids)); + } + } + return existingId; + } + + String entityId = UUID.randomUUID().toString(); + List embedding = embeddingClient.embed(normalized); + + Map metadata = new HashMap<>(); + if (relationIds != null && !relationIds.isEmpty()) metadata.put("relation_ids", relationIds); + if (passageIds != null && !passageIds.isEmpty()) metadata.put("passage_ids", passageIds); + + List> metadatas = metadata.isEmpty() ? null : List.of(metadata); + store.insertEntities(List.of(normalized), List.of(entityId), + List.of(embedding), metadatas, false); + + entityNameToId.put(normalized, entityId); + return entityId; + } + + // ==================== SubGraph Creation ==================== + + public SubGraph createSubGraph() { + return new SubGraph(store); + } + + // ==================== Collection Management ==================== + + public void createCollections(boolean dropExisting) { + store.createCollections(dropExisting); + } + + public void dropCollections() { + store.dropCollections(); + entityNameToId.clear(); + relationTextToId.clear(); + } + + public void reset() { + dropCollections(); + store.createCollections(true); + } + + // ==================== Helpers ==================== + + @SuppressWarnings("unchecked") + private List safeGetList(Map map, String key) { + Object val = map.get(key); + if (val instanceof List) { + return ((List) val).stream().map(Object::toString).collect(Collectors.toList()); + } + return new ArrayList<>(); + } + + private String safeGet(Map map, String key) { + Object val = map.get(key); + return val != null ? val.toString() : ""; + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/GraphBuilder.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/GraphBuilder.java new file mode 100644 index 0000000..d2c72e0 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/GraphBuilder.java @@ -0,0 +1,200 @@ +package com.zjkl.vectorgraphrag.graph; + +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import com.zjkl.vectorgraphrag.model.*; +import lombok.Getter; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Builds in-memory graph structures from documents with extracted triplets. + * Manages entity-relation-passage adjacency mappings. + */ +public class GraphBuilder { + + private final VectorGraphRagSettings settings; + + // id -> text + @Getter + private final Map entities = new LinkedHashMap<>(); + @Getter + private final Map relations = new LinkedHashMap<>(); + @Getter + private final Map passages = new LinkedHashMap<>(); + + // ordered IDs + @Getter + private final List entityIds = new ArrayList<>(); + @Getter + private final List relationIds = new ArrayList<>(); + @Getter + private final List passageIds = new ArrayList<>(); + + // deduplication + private final Map entityNameToId = new HashMap<>(); + private final Map relationTextToId = new HashMap<>(); + + // triplet storage + @Getter + private final Map relationIdToTriplet = new HashMap<>(); + + // adjacency + @Getter + private final Map> entityToRelationIds = new HashMap<>(); + @Getter + private final Map> entityToPassageIds = new HashMap<>(); + @Getter + private final Map> relationToPassageIds = new HashMap<>(); + @Getter + private final Map> relationToEntityIds = new HashMap<>(); + @Getter + private final Map> passageToEntityIds = new HashMap<>(); + @Getter + private final Map> passageToRelationIds = new HashMap<>(); + + public GraphBuilder(VectorGraphRagSettings settings) { + this.settings = settings; + } + + public void clear() { + entities.clear(); + relations.clear(); + passages.clear(); + entityIds.clear(); + relationIds.clear(); + passageIds.clear(); + entityNameToId.clear(); + relationTextToId.clear(); + relationIdToTriplet.clear(); + entityToRelationIds.clear(); + entityToPassageIds.clear(); + relationToPassageIds.clear(); + relationToEntityIds.clear(); + passageToEntityIds.clear(); + passageToRelationIds.clear(); + } + + public ExtractionResult buildFromDocuments(List documents) { + clear(); + + for (Document doc : documents) { + String passageId = doc.getId() != null ? doc.getId() : UUID.randomUUID().toString(); + passages.put(passageId, doc.getText()); + passageIds.add(passageId); + if (doc.getId() == null) doc.setId(passageId); + + // Process triplets + List triplets = doc.getTriplets(); + if (triplets == null) triplets = List.of(); + + // Also check metadata for raw triplets + Object rawTriplets = doc.getMetadata().get("triplets"); + if (rawTriplets instanceof List) { + for (Object raw : (List) rawTriplets) { + if (raw instanceof List && ((List) raw).size() >= 3) { + List parts = (List) raw; + Triplet t = new Triplet( + String.valueOf(parts.get(0)), + String.valueOf(parts.get(1)), + String.valueOf(parts.get(2)) + ); + addRelation(t, passageId); + } else if (raw instanceof Triplet) { + addRelation((Triplet) raw, passageId); + } + } + } else { + for (Triplet t : triplets) { + addRelation(t, passageId); + } + } + } + + // Build result + List entityList = entityIds.stream() + .map(eid -> Entity.builder().id(eid).name(entities.get(eid)).build()) + .collect(Collectors.toList()); + + List relationList = relationIds.stream() + .map(rid -> Relation.builder() + .id(rid) + .text(relations.get(rid)) + .triplet(relationIdToTriplet.get(rid)) + .sourcePassageIds(new ArrayList<>(relationToPassageIds.getOrDefault(rid, List.of()))) + .build()) + .collect(Collectors.toList()); + + return ExtractionResult.builder() + .documents(documents.stream().map(Document::getText).collect(Collectors.toList())) + .entities(entityList) + .relations(relationList) + .entityToRelationIds(new HashMap<>(entityToRelationIds)) + .relationToPassageIds(new HashMap<>(relationToPassageIds)) + .build(); + } + + private String addRelation(Triplet triplet, String passageId) { + String subject = normalizePhrase(triplet.getSubject()); + String predicate = normalizePhrase(triplet.getPredicate()); + String obj = normalizePhrase(triplet.getObject()); + String relationText = subject + " " + predicate + " " + obj; + + String relationId = relationTextToId.get(relationText); + if (relationId == null) { + relationId = UUID.randomUUID().toString(); + relations.put(relationId, relationText); + relationIds.add(relationId); + relationTextToId.put(relationText, relationId); + relationIdToTriplet.put(relationId, triplet); + + String subjectId = addEntity(triplet.getSubject(), passageId); + String objectId = addEntity(triplet.getObject(), passageId); + + entityToRelationIds.computeIfAbsent(subjectId, k -> new ArrayList<>()).add(relationId); + entityToRelationIds.computeIfAbsent(objectId, k -> new ArrayList<>()).add(relationId); + relationToEntityIds.put(relationId, List.of(subjectId, objectId)); + } + + // Link relation to passage + relationToPassageIds.computeIfAbsent(relationId, k -> new ArrayList<>()).add(passageId); + passageToRelationIds.computeIfAbsent(passageId, k -> new ArrayList<>()).add(relationId); + + return relationId; + } + + private String addEntity(String entityName, String passageId) { + String normalized = normalizePhrase(entityName); + String entityId = entityNameToId.get(normalized); + if (entityId == null) { + entityId = UUID.randomUUID().toString(); + entities.put(entityId, normalized); + entityIds.add(entityId); + entityNameToId.put(normalized, entityId); + } + + // Link entity to passage + entityToPassageIds.computeIfAbsent(entityId, k -> new ArrayList<>()).add(passageId); + passageToEntityIds.computeIfAbsent(passageId, k -> new ArrayList<>()).add(entityId); + + return entityId; + } + + public static String normalizePhrase(String phrase) { + if (phrase == null) return ""; + // Replace all non-alphanumeric chars with space, lowercase, trim + return phrase.replaceAll("[^A-Za-z0-9 ]", " ").toLowerCase().trim(); + } + + public List getEntityTexts() { + return entityIds.stream().map(entities::get).collect(Collectors.toList()); + } + + public List getRelationTexts() { + return relationIds.stream().map(relations::get).collect(Collectors.toList()); + } + + public List getPassageTexts() { + return passageIds.stream().map(passages::get).collect(Collectors.toList()); + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/GraphRetriever.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/GraphRetriever.java new file mode 100644 index 0000000..c2ef47a --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/GraphRetriever.java @@ -0,0 +1,237 @@ +package com.zjkl.vectorgraphrag.graph; + +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import com.zjkl.vectorgraphrag.llm.EntityExtractor; +import com.zjkl.vectorgraphrag.model.RetrievalResult; +import com.zjkl.vectorgraphrag.storage.EmbeddingClient; +import com.zjkl.vectorgraphrag.storage.MilvusStore; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Graph-based retriever using multi-way vector search. + * Performs entity and relation retrieval, then expands the subgraph + * with lazy loading from Milvus. + */ +@Slf4j +public class GraphRetriever { + + private final VectorGraphRagSettings settings; + private final MilvusStore store; + private final EmbeddingClient embeddingClient; + private final EntityExtractor entityExtractor; + + public GraphRetriever(VectorGraphRagSettings settings, MilvusStore store, + EmbeddingClient embeddingClient, EntityExtractor entityExtractor) { + this.settings = settings; + this.store = store; + this.embeddingClient = embeddingClient; + this.entityExtractor = entityExtractor; + } + + public RetrievalResult retrieve(String query) { + return retrieve(query, null, null, null, null, null, null); + } + + public RetrievalResult retrieve(String query, + Integer entityTopK, + Integer relationTopK, + Float entitySimilarityThreshold, + Float relationSimilarityThreshold, + Integer expansionDegree, + String filter) { + Set allowedPassageIds = getAllowedPassageIds(filter); + + // Extract query entities + List queryEntities = entityExtractor.extract(query); + + // Retrieve entities + List entityIds = new ArrayList<>(); + List entityTexts = new ArrayList<>(); + List entityScores = new ArrayList<>(); + + if (!queryEntities.isEmpty()) { + List> queryEmbeddings = embeddingClient.embedBatch(queryEntities); + int eTopK = entityTopK != null ? entityTopK : settings.getEntityTopK(); + float eThreshold = entitySimilarityThreshold != null ? entitySimilarityThreshold + : settings.getEntitySimilarityThreshold(); + + List> searchResults = store.searchEntities(queryEmbeddings, eTopK); + Set seenIds = new LinkedHashSet<>(); + + for (Map result : searchResults) { + double distance = (double) result.get("distance"); + if (distance <= eThreshold) continue; + + @SuppressWarnings("unchecked") + Map entity = (Map) result.get("entity"); + String eid = entity.get("id").toString(); + if (seenIds.add(eid)) { + entityIds.add(eid); + entityTexts.add(entity.get("text").toString()); + entityScores.add((float) distance); + } + } + } + + // Retrieve relations + int rTopK = relationTopK != null ? relationTopK : settings.getRelationTopK(); + float rThreshold = relationSimilarityThreshold != null ? relationSimilarityThreshold + : settings.getRelationSimilarityThreshold(); + + List queryEmbedding = embeddingClient.embed(query); + List> relationResults = store.searchRelations(queryEmbedding, rTopK); + + List relationIds = new ArrayList<>(); + List relationTexts = new ArrayList<>(); + List relationScores = new ArrayList<>(); + + for (Map result : relationResults) { + double distance = (double) result.get("distance"); + if (distance <= rThreshold) continue; + + @SuppressWarnings("unchecked") + Map entity = (Map) result.get("entity"); + String rid = entity.get("id").toString(); + relationIds.add(rid); + relationTexts.add(entity.get("text").toString()); + relationScores.add((float) distance); + } + + // Filter relations by allowed passage IDs + if (allowedPassageIds != null) { + List filteredIds = filterRelationsByPassageIds(relationIds, allowedPassageIds); + // Rebuild parallel lists + Set allowed = new HashSet<>(filteredIds); + List keptIds = new ArrayList<>(); + List keptTexts = new ArrayList<>(); + List keptScores = new ArrayList<>(); + for (int i = 0; i < relationIds.size(); i++) { + if (allowed.contains(relationIds.get(i))) { + keptIds.add(relationIds.get(i)); + keptTexts.add(relationTexts.get(i)); + keptScores.add(relationScores.get(i)); + } + } + relationIds = keptIds; + relationTexts = keptTexts; + relationScores = keptScores; + } + + // Expand subgraph + SubGraph subgraph = expandSubGraph(entityIds, relationIds, expansionDegree); + + // Apply eviction + int threshold = settings.getRelationNumberThreshold(); + List filteredExpandedIds = filterRelationsByPassageIds( + new ArrayList<>(subgraph.getRelationIds()), allowedPassageIds); + List expandedIds = new ArrayList<>(); + List expandedTexts = new ArrayList<>(); + boolean evictionOccurred = false; + int evictionBefore = filteredExpandedIds.size(); + + if (!filteredExpandedIds.isEmpty()) { + if (filteredExpandedIds.size() <= threshold) { + // No eviction needed + expandedIds = filteredExpandedIds; + expandedTexts = filteredExpandedIds.stream() + .map(rid -> { + SubGraphRelation rel = null; + for (SubGraphRelation r : subgraph.getRelations()) { + if (r.getId().equals(rid)) { rel = r; break; } + } + return rel != null ? rel.getText() : ""; + }) + .collect(Collectors.toList()); + } else { + // Eviction: use vector search to filter + log.info("Use Eviction Strategy. ({} -> {})", evictionBefore, threshold); + List qEmbed = embeddingClient.embed(query); + String idsStr = filteredExpandedIds.stream() + .map(id -> "\"" + id + "\"") + .collect(Collectors.joining(", ")); + String filterExpr = "id in [" + idsStr + "]"; + + List> searchRes = store.searchRelationsWithFilter(qEmbed, threshold, filterExpr); + evictionOccurred = true; + for (Map r : searchRes) { + expandedIds.add(r.get("id").toString()); + expandedTexts.add(r.get("text") != null ? r.get("text").toString() : ""); + } + } + } + + return RetrievalResult.builder() + .entityIds(entityIds) + .entityTexts(entityTexts) + .entityScores(entityScores) + .relationIds(relationIds) + .relationTexts(relationTexts) + .relationScores(relationScores) + .subgraph(subgraph) + .expandedRelationIds(expandedIds) + .expandedRelationTexts(expandedTexts) + .query(query) + .queryEntities(queryEntities) + .evictionOccurred(evictionOccurred) + .evictionBeforeCount(evictionBefore) + .evictionAfterCount(expandedIds.size()) + .build(); + } + + public List retrievePassagesNaive(String query, int topK, String filter) { + List queryEmbedding = embeddingClient.embed(query); + List> results = store.searchPassages(queryEmbedding, topK, filter); + return results.stream() + .map(r -> { + @SuppressWarnings("unchecked") + Map entity = (Map) r.get("entity"); + return entity.get("text").toString(); + }) + .collect(Collectors.toList()); + } + + private SubGraph expandSubGraph(List entityIds, List relationIds, Integer degree) { + int deg = degree != null ? degree : settings.getExpansionDegree(); + SubGraph subgraph = new SubGraph(store); + subgraph.addEntities(entityIds); + subgraph.addRelations(relationIds); + subgraph.expand(deg); + return subgraph; + } + + private Set getAllowedPassageIds(String filter) { + if (filter == null || filter.trim().isEmpty()) return null; + return new HashSet<>(store.queryPassageIds(filter)); + } + + private List filterRelationsByPassageIds(List relationIds, Set allowedPassageIds) { + if (allowedPassageIds == null) return relationIds; + if (relationIds == null || relationIds.isEmpty()) return List.of(); + + List> relationData = store.getRelationsByIds(relationIds); + Set allowed = new HashSet<>(); + for (Map r : relationData) { + List pids = safeGetList(r, "passage_ids"); + for (String pid : pids) { + if (allowedPassageIds.contains(pid)) { + allowed.add(r.get("id").toString()); + break; + } + } + } + + return relationIds.stream().filter(allowed::contains).collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + private List safeGetList(Map map, String key) { + Object val = map.get(key); + if (val instanceof List) { + return ((List) val).stream().map(Object::toString).collect(Collectors.toList()); + } + return List.of(); + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraph.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraph.java new file mode 100644 index 0000000..85f0ab0 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraph.java @@ -0,0 +1,298 @@ +package com.zjkl.vectorgraphrag.graph; + +import com.zjkl.vectorgraphrag.storage.MilvusStore; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Lazy-loading subgraph for graph expansion. + * Starts from seed entities/relations and expands by fetching + * neighbor data on-demand from Milvus storage. + */ +@Slf4j +public class SubGraph { + + private final MilvusStore store; + + private final Set entityIds = new LinkedHashSet<>(); + private final Set relationIds = new LinkedHashSet<>(); + private final Set passageIds = new LinkedHashSet<>(); + + private final Map entityMap = new HashMap<>(); + private final Map relationMap = new HashMap<>(); + private final Map passageMap = new HashMap<>(); + + @Getter + private final List> expansionHistory = new ArrayList<>(); + + public SubGraph(MilvusStore store) { + this.store = store; + } + + // ==================== Add Initial Nodes ==================== + + public SubGraph addEntities(List ids) { + Set newIds = new LinkedHashSet<>(ids); + newIds.removeAll(entityIds); + if (!newIds.isEmpty()) { + entityIds.addAll(newIds); + fetchEntities(new ArrayList<>(newIds)); + + if (expansionHistory.isEmpty()) { + Map step = new LinkedHashMap<>(); + step.put("step", 0); + step.put("operation", "init"); + step.put("addedEntityIds", new ArrayList<>(newIds)); + step.put("addedRelationIds", List.of()); + expansionHistory.add(step); + } else { + @SuppressWarnings("unchecked") + List added = (List) expansionHistory.get(0).get("addedEntityIds"); + added.addAll(newIds); + } + } + return this; + } + + public SubGraph addRelations(List ids) { + Set newIds = new LinkedHashSet<>(ids); + newIds.removeAll(relationIds); + if (!newIds.isEmpty()) { + relationIds.addAll(newIds); + fetchRelations(new ArrayList<>(newIds)); + + if (expansionHistory.isEmpty()) { + Map step = new LinkedHashMap<>(); + step.put("step", 0); + step.put("operation", "init"); + step.put("addedEntityIds", List.of()); + step.put("addedRelationIds", new ArrayList<>(newIds)); + expansionHistory.add(step); + } else { + @SuppressWarnings("unchecked") + List added = (List) expansionHistory.get(0).get("addedRelationIds"); + added.addAll(newIds); + } + } + return this; + } + + // ==================== Expansion ==================== + + public SubGraph expand(int degree) { + // Step 0: From initial entities -> relations, merge with initial relations + Set initNewRelations = new LinkedHashSet<>(); + for (String eid : entityIds) { + SubGraphEntity entity = entityMap.get(eid); + if (entity != null) { + for (String rid : entity.getRelationIds()) { + if (!relationIds.contains(rid)) { + initNewRelations.add(rid); + } + } + } + } + + if (!initNewRelations.isEmpty()) { + fetchRelations(new ArrayList<>(initNewRelations)); + relationIds.addAll(initNewRelations); + } + + Map initStep = new LinkedHashMap<>(); + initStep.put("step", expansionHistory.size()); + initStep.put("operation", "init_merge"); + initStep.put("newRelationIds", new ArrayList<>(initNewRelations)); + initStep.put("totalEntities", entityIds.size()); + initStep.put("totalRelations", relationIds.size()); + expansionHistory.add(initStep); + + // For each degree: relations -> entities -> relations + for (int step = 0; step < degree; step++) { + Set stepNewEntities = new LinkedHashSet<>(); + Set stepNewRelations = new LinkedHashSet<>(); + + // From current relations -> entities + for (String rid : relationIds) { + SubGraphRelation relation = relationMap.get(rid); + if (relation != null) { + for (String eid : relation.getEntityIds()) { + if (!entityIds.contains(eid)) { + stepNewEntities.add(eid); + } + } + } + } + + // Fetch new entities + if (!stepNewEntities.isEmpty()) { + fetchEntities(new ArrayList<>(stepNewEntities)); + entityIds.addAll(stepNewEntities); + } + + // From new entities -> relations + for (String eid : stepNewEntities) { + SubGraphEntity entity = entityMap.get(eid); + if (entity != null) { + for (String rid : entity.getRelationIds()) { + if (!relationIds.contains(rid)) { + stepNewRelations.add(rid); + } + } + } + } + + // Fetch new relations + if (!stepNewRelations.isEmpty()) { + fetchRelations(new ArrayList<>(stepNewRelations)); + relationIds.addAll(stepNewRelations); + } + + Map stepRecord = new LinkedHashMap<>(); + stepRecord.put("step", expansionHistory.size()); + stepRecord.put("operation", "expand_degree_" + (step + 1)); + stepRecord.put("newEntityIds", new ArrayList<>(stepNewEntities)); + stepRecord.put("newRelationIds", new ArrayList<>(stepNewRelations)); + stepRecord.put("totalEntities", entityIds.size()); + stepRecord.put("totalRelations", relationIds.size()); + expansionHistory.add(stepRecord); + } + + // Collect passages from all relations + for (String rid : relationIds) { + SubGraphRelation relation = relationMap.get(rid); + if (relation != null) { + passageIds.addAll(relation.getPassageIds()); + } + } + + // Fetch passages + if (!passageIds.isEmpty()) { + fetchPassages(new ArrayList<>(passageIds)); + } + + return this; + } + + // ==================== Data Fetching ==================== + + private void fetchEntities(List ids) { + List toFetch = ids.stream() + .filter(id -> !entityMap.containsKey(id)) + .collect(Collectors.toList()); + if (toFetch.isEmpty()) return; + + List> results = store.getEntitiesByIds(toFetch); + for (Map r : results) { + SubGraphEntity entity = SubGraphEntity.builder() + .id(safeGet(r, "id")) + .name(safeGet(r, "text")) + .relationIds(safeGetList(r, "relation_ids")) + .passageIds(safeGetList(r, "passage_ids")) + .build(); + entityMap.put(entity.getId(), entity); + } + } + + private void fetchRelations(List ids) { + List toFetch = ids.stream() + .filter(id -> !relationMap.containsKey(id)) + .collect(Collectors.toList()); + if (toFetch.isEmpty()) return; + + List> results = store.getRelationsByIds(toFetch); + for (Map r : results) { + String text = safeGet(r, "text"); + String subject = r.getOrDefault("subject", "").toString(); + String predicate = r.getOrDefault("predicate", "").toString(); + String obj = r.getOrDefault("object", "").toString(); + + // Fallback parsing + if (subject.isEmpty() && predicate.isEmpty() && obj.isEmpty()) { + String[] parts = text.split(" ", 3); + subject = parts.length > 0 ? parts[0] : ""; + predicate = parts.length > 1 ? parts[1] : ""; + obj = parts.length > 2 ? parts[2] : ""; + } + + SubGraphRelation relation = SubGraphRelation.builder() + .id(safeGet(r, "id")) + .text(text) + .subject(subject) + .predicate(predicate) + .object(obj) + .entityIds(safeGetList(r, "entity_ids")) + .passageIds(safeGetList(r, "passage_ids")) + .build(); + relationMap.put(relation.getId(), relation); + } + } + + private void fetchPassages(List ids) { + List toFetch = ids.stream() + .filter(id -> !passageMap.containsKey(id)) + .collect(Collectors.toList()); + if (toFetch.isEmpty()) return; + + List> results = store.getPassagesByIds(toFetch); + for (Map r : results) { + SubGraphPassage passage = SubGraphPassage.builder() + .id(safeGet(r, "id")) + .text(safeGet(r, "text")) + .entityIds(safeGetList(r, "entity_ids")) + .relationIds(safeGetList(r, "relation_ids")) + .build(); + passageMap.put(passage.getId(), passage); + } + } + + // ==================== Accessors ==================== + + public Set getEntityIds() { return Collections.unmodifiableSet(entityIds); } + public Set getRelationIds() { return Collections.unmodifiableSet(relationIds); } + public Set getPassageIds() { return Collections.unmodifiableSet(passageIds); } + + public List getEntities() { + return entityIds.stream().map(entityMap::get).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public List getRelations() { + return relationIds.stream().map(relationMap::get).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public List getPassages() { + return passageIds.stream().map(passageMap::get).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public List getEntityNames() { + return getEntities().stream().map(SubGraphEntity::getName).collect(Collectors.toList()); + } + + public List getRelationTexts() { + return getRelations().stream().map(SubGraphRelation::getText).collect(Collectors.toList()); + } + + public List getPassageTexts() { + return getPassages().stream().map(SubGraphPassage::getText).collect(Collectors.toList()); + } + + // ==================== Helpers ==================== + + @SuppressWarnings("unchecked") + private List safeGetList(Map map, String key) { + Object val = map.get(key); + if (val instanceof List) { + return ((List) val).stream() + .map(Object::toString) + .collect(Collectors.toList()); + } + return List.of(); + } + + private String safeGet(Map map, String key) { + Object val = map.get(key); + return val != null ? val.toString() : ""; + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphEntity.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphEntity.java new file mode 100644 index 0000000..b4f7091 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphEntity.java @@ -0,0 +1,22 @@ +package com.zjkl.vectorgraphrag.graph; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubGraphEntity { + private String id; + private String name; + @Builder.Default + private List relationIds = new ArrayList<>(); + @Builder.Default + private List passageIds = new ArrayList<>(); +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphPassage.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphPassage.java new file mode 100644 index 0000000..7b0fe08 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphPassage.java @@ -0,0 +1,22 @@ +package com.zjkl.vectorgraphrag.graph; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubGraphPassage { + private String id; + private String text; + @Builder.Default + private List entityIds = new ArrayList<>(); + @Builder.Default + private List relationIds = new ArrayList<>(); +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphRelation.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphRelation.java new file mode 100644 index 0000000..5dfd76c --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/graph/SubGraphRelation.java @@ -0,0 +1,25 @@ +package com.zjkl.vectorgraphrag.graph; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubGraphRelation { + private String id; + private String text; + private String subject; + private String predicate; + private String object; + @Builder.Default + private List entityIds = new ArrayList<>(); + @Builder.Default + private List passageIds = new ArrayList<>(); +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/AnswerGenerator.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/AnswerGenerator.java new file mode 100644 index 0000000..9f5c5a9 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/AnswerGenerator.java @@ -0,0 +1,45 @@ +package com.zjkl.vectorgraphrag.llm; + +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * Generates answers using retrieved passage context via LLM. + */ +@Slf4j +public class AnswerGenerator { + + private static final String ANSWER_PROMPT = + "Use the following pieces of retrieved context to answer the question. " + + "If there is not enough information in the retrieved context to answer the question, " + + "just say that you don't know.\n\n" + + "Question: {question}\n\n" + + "Context: {context}\n\n" + + "Answer:"; + + private final OpenAiClient openAiClient; + private final VectorGraphRagSettings settings; + + public AnswerGenerator(VectorGraphRagSettings settings, OpenAiClient openAiClient) { + this.settings = settings; + this.openAiClient = openAiClient; + } + + public String generate(String question, List passages) { + String context = String.join("\n\n", passages); + String prompt = ANSWER_PROMPT + .replace("{question}", question) + .replace("{context}", context); + + try { + return openAiClient.chatWithMessages(List.of( + java.util.Map.of("role", "user", "content", prompt) + )); + } catch (Exception e) { + log.warn("Answer generation failed: {}", e.getMessage()); + return "I don't know."; + } + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/EntityExtractor.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/EntityExtractor.java new file mode 100644 index 0000000..6af1989 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/EntityExtractor.java @@ -0,0 +1,82 @@ +package com.zjkl.vectorgraphrag.llm; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.zjkl.vectorgraphrag.graph.GraphBuilder.normalizePhrase; + +/** + * Extracts named entities from text for query processing (NER). + * Used during query time to identify entities in the user's question. + */ +@Slf4j +public class EntityExtractor { + + private static final String SYSTEM_PROMPT = + "You're a very effective entity extraction system."; + + private static final String ONE_SHOT_INPUT = + "Please extract all named entities that are important for solving the questions below.\n" + + "Place the named entities in json format.\n\n" + + "Question: Which magazine was started first Arthur's Magazine or First for Women?"; + + private static final String ONE_SHOT_OUTPUT = + "{\"named_entities\": [\"First for Women\", \"Arthur's Magazine\"]}"; + + private static final String TEMPLATE = "\nQuestion: {}\n"; + + private final OpenAiClient openAiClient; + private final Gson gson; + + public EntityExtractor(VectorGraphRagSettings settings, OpenAiClient openAiClient) { + this.openAiClient = openAiClient; + this.gson = new Gson(); + } + + public List extract(String question) { + if (question == null || question.trim().isEmpty()) return List.of(); + + try { + List> examples = List.of( + Map.of("user", ONE_SHOT_INPUT, "assistant", ONE_SHOT_OUTPUT) + ); + String response = openAiClient.chat(SYSTEM_PROMPT, examples, + TEMPLATE.replace("{}", question)); + + return parseResponse(response); + } catch (Exception e) { + log.warn("Entity extraction failed, returning empty: {}", e.getMessage()); + return List.of(); + } + } + + private List parseResponse(String response) { + try { + JsonObject json = gson.fromJson(response, JsonObject.class); + JsonArray entities = json.getAsJsonArray("named_entities"); + if (entities == null) { + entities = json.getAsJsonArray("entities"); + } + if (entities == null) return List.of(); + + List results = new ArrayList<>(); + for (int i = 0; i < entities.size(); i++) { + String entity = entities.get(i).getAsString(); + if (entity != null && !entity.trim().isEmpty()) { + results.add(normalizePhrase(entity)); + } + } + return results; + } catch (Exception e) { + log.warn("Failed to parse NER response: {}", e.getMessage()); + return List.of(); + } + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/LLMCache.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/LLMCache.java new file mode 100644 index 0000000..386ab6b --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/LLMCache.java @@ -0,0 +1,74 @@ +package com.zjkl.vectorgraphrag.llm; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.TimeUnit; + +/** + * LLM response cache. + * Uses Caffeine in-memory cache with file-backed persistence. + * Each model has its own cache namespace. + */ +@Slf4j +public class LLMCache { + + private final Cache cache; + + public LLMCache() { + this(10000, 24, TimeUnit.HOURS); + } + + public LLMCache(int maxSize, long duration, TimeUnit unit) { + this.cache = Caffeine.newBuilder() + .maximumSize(maxSize) + .expireAfterWrite(duration, unit) + .recordStats() + .build(); + } + + public String get(String modelName, String prompt, double temperature) { + String key = buildKey(modelName, prompt, temperature); + String result = cache.getIfPresent(key); + if (result != null) { + log.debug("LLM cache hit for model={}", modelName); + } + return result; + } + + public void set(String modelName, String prompt, String response, double temperature) { + String key = buildKey(modelName, prompt, temperature); + cache.put(key, response); + } + + public void clear() { + cache.invalidateAll(); + } + + public long size() { + return cache.estimatedSize(); + } + + private String buildKey(String modelName, String prompt, double temperature) { + String raw = modelName + "::" + prompt + "::temp=" + temperature; + return md5(raw); + } + + private String md5(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("MD5"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(); + for (byte b : hash) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not available", e); + } + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/LLMReranker.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/LLMReranker.java new file mode 100644 index 0000000..f5c2e57 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/LLMReranker.java @@ -0,0 +1,180 @@ +package com.zjkl.vectorgraphrag.llm; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * LLM-based reranker for candidate relations. + * Uses few-shot prompting with chain-of-thought to select the most relevant relations. + */ +@Slf4j +public class LLMReranker { + + // 3 few-shot examples for 1-hop, 2-hop, and 3-hop reasoning + private static final String EXAMPLE_1_INPUT = + "I will provide you with a set of relationship descriptions from a knowledge graph. Select exactly 5 relationships most useful for answering this multi-hop question.\n\n" + + "Return JSON with \"thought_process\" and \"useful_relations\" (list of 5 relation lines, most useful first).\n\n" + + "Question:\nWhen did Lothair Ii's mother die?\n\n" + + "Relationship descriptions:\n" + + "[53] bertha married to theobald of arles\n" + + "[54] bertha married to adalbert ii of tuscany\n" + + "[42] lothair ii son of ermengarde of tours\n" + + "[43] lothair ii married to teutberga\n" + + "[41] lothair ii son of emperor lothair i\n" + + "[60] lothair ii husband of waldrada\n" + + "[67] waldrada was mistress of lothair ii\n"; + + private static final String EXAMPLE_1_OUTPUT = + "{\"thought_process\": \"2-hop question: First find Lothair II's mother (relation [42]: Ermengarde of Tours), then find death date. [41] gives father for family context.\", " + + "\"useful_relations\": [\"[42] lothair ii son of ermengarde of tours\", \"[41] lothair ii son of emperor lothair i\", \"[43] lothair ii married to teutberga\", \"[60] lothair ii husband of waldrada\", \"[67] waldrada was mistress of lothair ii\"]}"; + + private static final String EXAMPLE_2_INPUT = + "I will provide you with a set of relationship descriptions from a knowledge graph. Select exactly 5 relationships most useful for answering this multi-hop question.\n\n" + + "Return JSON with \"thought_process\" and \"useful_relations\" (list of 5 relation lines, most useful first).\n\n" + + "Question:\nWhat country is the composer of \"Erta Eterna\" from?\n\n" + + "Relationship descriptions:\n" + + "[12] terra eterna composed by paulo flores\n" + + "[15] paulo flores born in angola\n" + + "[18] paulo flores genre is semba\n" + + "[22] angola located in africa\n" + + "[25] semba originated in angola\n" + + "[30] paulo flores nationality angolan\n"; + + private static final String EXAMPLE_2_OUTPUT = + "{\"thought_process\": \"2-hop question: First find composer of Terra Eterna ([12]: Paulo Flores), then find his country ([15] born in Angola or [30] nationality Angolan).\", " + + "\"useful_relations\": [\"[12] terra eterna composed by paulo flores\", \"[15] paulo flores born in angola\", \"[30] paulo flores nationality angolan\", \"[22] angola located in africa\", \"[25] semba originated in angola\"]}"; + + private static final String EXAMPLE_3_INPUT = + "I will provide you with a set of relationship descriptions from a knowledge graph. Select exactly 5 relationships most useful for answering this multi-hop question.\n\n" + + "Return JSON with \"thought_process\" and \"useful_relations\" (list of 5 relation lines, most useful first).\n\n" + + "Question:\nWho is the director of the film that won the award also won by \"The Hurt Locker\"?\n\n" + + "Relationship descriptions:\n" + + "[5] the hurt locker won academy award best picture\n" + + "[8] the hurt locker directed by kathryn bigelow\n" + + "[12] moonlight won academy award best picture\n" + + "[15] moonlight directed by barry jenkins\n" + + "[20] la la land won golden globe best musical\n" + + "[25] barry jenkins born in miami\n"; + + private static final String EXAMPLE_3_OUTPUT = + "{\"thought_process\": \"3-hop question: (1) Find award won by The Hurt Locker ([5]: Academy Award Best Picture), (2) Find another film with same award ([12]: Moonlight), (3) Find director ([15]: Barry Jenkins).\", " + + "\"useful_relations\": [\"[5] the hurt locker won academy award best picture\", \"[12] moonlight won academy award best picture\", \"[15] moonlight directed by barry jenkins\", \"[8] the hurt locker directed by kathryn bigelow\", \"[25] barry jenkins born in miami\"]}"; + + private final OpenAiClient openAiClient; + private final Gson gson; + private final VectorGraphRagSettings settings; + + public LLMReranker(VectorGraphRagSettings settings, OpenAiClient openAiClient) { + this.settings = settings; + this.openAiClient = openAiClient; + this.gson = new Gson(); + } + + public Map.Entry, List> rerank(String query, + List relationIds, + List relationTexts) { + if (relationIds == null || relationIds.isEmpty()) { + return Map.entry(List.of(), List.of()); + } + + // Format relations + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < relationIds.size(); i++) { + sb.append("[").append(relationIds.get(i)).append("] ") + .append(relationTexts.get(i)).append("\n"); + } + String relationDescriptions = sb.toString(); + + // Build prompt + String userPrompt = "Question:\n" + query + "\n\nRelationship descriptions:\n" + relationDescriptions; + + try { + List> messages = new ArrayList<>(); + messages.add(Map.of("role", "user", "content", EXAMPLE_1_INPUT)); + messages.add(Map.of("role", "assistant", "content", EXAMPLE_1_OUTPUT)); + messages.add(Map.of("role", "user", "content", EXAMPLE_2_INPUT)); + messages.add(Map.of("role", "assistant", "content", EXAMPLE_2_OUTPUT)); + messages.add(Map.of("role", "user", "content", EXAMPLE_3_INPUT)); + messages.add(Map.of("role", "assistant", "content", EXAMPLE_3_OUTPUT)); + messages.add(Map.of("role", "user", "content", userPrompt)); + + String response = openAiClient.chatWithMessages(messages); + return parseResponse(response, new HashSet<>(relationIds), relationIds, relationTexts); + + } catch (Exception e) { + log.warn("Reranking failed, using top relations: {}", e.getMessage()); + int limit = Math.min(settings.getFinalTopK(), relationIds.size()); + return Map.entry( + relationIds.subList(0, limit), + relationTexts.subList(0, limit) + ); + } + } + + private Map.Entry, List> parseResponse(String response, + Set validIds, + List allIds, + List allTexts) { + try { + JsonObject json = gson.fromJson(response, JsonObject.class); + JsonArray usefulRelations = json.getAsJsonArray("useful_relations"); + if (usefulRelations == null) { + return Map.entry(List.of(), List.of()); + } + + // Build id->text lookup + Map idToText = new LinkedHashMap<>(); + for (int i = 0; i < allIds.size(); i++) { + idToText.put(allIds.get(i), allTexts.get(i)); + } + + List selectedIds = new ArrayList<>(); + Set seen = new HashSet<>(); + + for (int i = 0; i < usefulRelations.size(); i++) { + String line = usefulRelations.get(i).getAsString(); + String relId = extractIdFromLine(line); + if (relId == null) continue; + + if (validIds.contains(relId) && seen.add(relId)) { + selectedIds.add(relId); + } else if (!validIds.contains(relId)) { + // Try to correct by text matching + String lineText = line.substring(line.indexOf("]") + 1).trim(); + for (int j = 0; j < allTexts.size(); j++) { + if (allTexts.get(j).trim().equalsIgnoreCase(lineText) + && seen.add(allIds.get(j))) { + selectedIds.add(allIds.get(j)); + break; + } + } + } + } + + List selectedTexts = selectedIds.stream() + .map(id -> idToText.getOrDefault(id, "")) + .collect(Collectors.toList()); + + return Map.entry(selectedIds, selectedTexts); + + } catch (Exception e) { + log.warn("Failed to parse reranker response: {}", e.getMessage()); + return Map.entry(List.of(), List.of()); + } + } + + private String extractIdFromLine(String line) { + int start = line.indexOf("["); + int end = line.indexOf("]"); + if (start >= 0 && end > start) { + return line.substring(start + 1, end).trim(); + } + return null; + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/OpenAiClient.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/OpenAiClient.java new file mode 100644 index 0000000..44cfb1a --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/OpenAiClient.java @@ -0,0 +1,281 @@ +package com.zjkl.vectorgraphrag.llm; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import lombok.extern.slf4j.Slf4j; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * OpenAI API client for chat completions and embeddings. + * Uses manual retry with exponential backoff (Spring @Retryable not used + * because this class is not a Spring-managed bean). + */ +@Slf4j +public class OpenAiClient { + + private final VectorGraphRagSettings settings; + private final HttpClient httpClient; + private final Gson gson; + private final LLMCache cache; + + private static final String DEFAULT_CHAT_URL = "https://api.openai.com/v1/chat/completions"; + private static final String DEFAULT_EMBED_URL = "https://api.openai.com/v1/embeddings"; + + public OpenAiClient(VectorGraphRagSettings settings, LLMCache cache) { + this.settings = settings; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(60)) + .build(); + this.gson = new Gson(); + this.cache = cache; + } + + /** + * Chat completion with structured output (response_format: json_object). + * Used by extractors and rerankers that need parsed JSON responses. + */ + public String chat(String systemPrompt, List> fewShotExamples, String userPrompt) { + String fullPrompt = systemPrompt + "\n" + userPrompt; + if (cache != null && settings.isUseLlmCache()) { + String cached = cache.get(settings.getLlmModel(), fullPrompt, settings.getLlmTemperature()); + if (cached != null) return cached; + } + + JsonArray messages = new JsonArray(); + messages.add(buildMessage("system", systemPrompt)); + for (Map example : fewShotExamples) { + messages.add(buildMessage("user", example.get("user"))); + messages.add(buildMessage("assistant", example.get("assistant"))); + } + messages.add(buildMessage("user", userPrompt)); + + String response = executeChat(messages, true); + + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + String content = jsonResponse.getAsJsonArray("choices") + .get(0).getAsJsonObject() + .get("message").getAsJsonObject() + .get("content").getAsString(); + + if (cache != null && settings.isUseLlmCache()) { + cache.set(settings.getLlmModel(), fullPrompt, content, settings.getLlmTemperature()); + } + + return content; + } + + /** + * Chat completion with free-form messages (no response_format restriction). + * Used by AnswerGenerator and similar free-text generation. + */ + public String chatWithMessages(List> messagesList) { + // Build cache key from the conversation + String fullPrompt = messagesList.stream() + .map(m -> m.get("role") + ": " + m.get("content")) + .collect(Collectors.joining("\n")); + + if (cache != null && settings.isUseLlmCache()) { + String cached = cache.get(settings.getLlmModel(), fullPrompt, settings.getLlmTemperature()); + if (cached != null) return cached; + } + + JsonArray messages = new JsonArray(); + for (Map msg : messagesList) { + messages.add(buildMessage(msg.get("role"), msg.get("content"))); + } + + String response = executeChat(messages, false); + + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + String content = jsonResponse.getAsJsonArray("choices") + .get(0).getAsJsonObject() + .get("message").getAsJsonObject() + .get("content").getAsString(); + + if (cache != null && settings.isUseLlmCache()) { + cache.set(settings.getLlmModel(), fullPrompt, content, settings.getLlmTemperature()); + } + + return content; + } + + /** + * Embed a single text string into a vector. + */ + public List embed(String text) { + JsonObject body = new JsonObject(); + body.addProperty("model", settings.getEmbeddingModel()); + body.addProperty("input", text); + + String response = executeHttp(buildEmbedUrl(), body.toString(), settings.getLlmMaxRetries()); + return parseEmbeddingResponse(response, 0); + } + + /** + * Embed multiple texts in a single API call (batch). + * Returns embeddings in the same order as input texts. + */ + public List> embedBatch(List texts) { + if (texts == null || texts.isEmpty()) return List.of(); + + JsonObject body = new JsonObject(); + body.addProperty("model", settings.getEmbeddingModel()); + JsonArray inputs = new JsonArray(); + for (String t : texts) inputs.add(t); + body.add("input", inputs); + + String response = executeHttp(buildEmbedUrl(), body.toString(), settings.getLlmMaxRetries()); + + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + JsonArray data = jsonResponse.getAsJsonArray("data"); + + // Index by the "index" field in the response to preserve order + List> results = new ArrayList<>(); + for (int i = 0; i < texts.size(); i++) results.add(null); + + for (int i = 0; i < data.size(); i++) { + JsonObject item = data.get(i).getAsJsonObject(); + int idx = item.get("index").getAsInt(); + JsonArray embedding = item.getAsJsonArray("embedding"); + List vec = new ArrayList<>(); + for (int j = 0; j < embedding.size(); j++) { + vec.add(embedding.get(j).getAsFloat()); + } + results.set(idx, vec); + } + return results; + } + + /** + * Build and execute a chat request. + */ + private String executeChat(JsonArray messages, boolean responseJson) { + JsonObject body = new JsonObject(); + body.addProperty("model", settings.getLlmModel()); + body.addProperty("temperature", settings.getLlmTemperature()); + body.add("messages", messages); + if (responseJson) { + JsonObject responseFormat = new JsonObject(); + responseFormat.addProperty("type", "json_object"); + body.add("response_format", responseFormat); + } + + return executeHttp(buildChatUrl(), body.toString(), settings.getLlmMaxRetries()); + } + + /** + * Build the chat completions URL, respecting custom baseUrl. + */ + private String buildChatUrl() { + String baseUrl = settings.getOpenaiBaseUrl(); + if (baseUrl != null && !baseUrl.isEmpty()) { + return (baseUrl.endsWith("/") ? baseUrl : baseUrl + "/") + "chat/completions"; + } + return DEFAULT_CHAT_URL; + } + + /** + * Build the embeddings URL, respecting custom baseUrl. + */ + private String buildEmbedUrl() { + String baseUrl = settings.getOpenaiBaseUrl(); + if (baseUrl != null && !baseUrl.isEmpty()) { + return (baseUrl.endsWith("/") ? baseUrl : baseUrl + "/") + "embeddings"; + } + return DEFAULT_EMBED_URL; + } + + /** + * Execute an HTTP POST with manual retry and exponential backoff. + * 4xx client errors are NOT retried; 5xx server errors and network failures are. + */ + private String executeHttp(String url, String bodyJson, int maxRetries) { + int attempt = 0; + while (true) { + try { + String apiKey = settings.getOpenaiApiKey(); + if (apiKey == null || apiKey.isEmpty()) { + throw new IllegalStateException("OpenAI API key is required"); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .timeout(Duration.ofSeconds(120)) + .POST(HttpRequest.BodyPublishers.ofString(bodyJson)) + .build(); + + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + return response.body(); + } + + if (response.statusCode() >= 400 && response.statusCode() < 500) { + // Client error — not retryable + throw new RuntimeException("OpenAI API client error: " + response.statusCode() + + " " + response.body()); + } + + // Server error — retry + attempt++; + if (attempt >= maxRetries) { + throw new RuntimeException("OpenAI API error after " + attempt + " retries: " + + response.statusCode() + " " + response.body()); + } + + long delay = Math.min(2000L * (long) Math.pow(1.5, attempt - 1), 10000); + log.warn("OpenAI API retry {}/{} after {}ms: status={}", attempt, maxRetries, delay, response.statusCode()); + Thread.sleep(delay); + + } catch (RuntimeException e) { + throw e; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("OpenAI API call interrupted", e); + } catch (Exception e) { + attempt++; + if (attempt >= maxRetries) { + throw new RuntimeException("OpenAI API call failed after " + attempt + " retries", e); + } + long delay = Math.min(2000L * (long) Math.pow(1.5, attempt - 1), 10000); + log.warn("OpenAI API retry {}/{} after {}ms: {}", attempt, maxRetries, delay, e.getMessage()); + try { Thread.sleep(delay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException(ie); } + } + } + } + + /** + * Parse a single embedding from the API response by index. + */ + private List parseEmbeddingResponse(String response, int index) { + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + JsonArray data = jsonResponse.getAsJsonArray("data"); + JsonArray embedding = data.get(index).getAsJsonObject().getAsJsonArray("embedding"); + List result = new ArrayList<>(embedding.size()); + for (int i = 0; i < embedding.size(); i++) { + result.add(embedding.get(i).getAsFloat()); + } + return result; + } + + private JsonObject buildMessage(String role, String content) { + JsonObject msg = new JsonObject(); + msg.addProperty("role", role); + msg.addProperty("content", content); + return msg; + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/TripletExtractor.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/TripletExtractor.java new file mode 100644 index 0000000..f4d29f2 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/llm/TripletExtractor.java @@ -0,0 +1,107 @@ +package com.zjkl.vectorgraphrag.llm; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import com.zjkl.vectorgraphrag.model.Document; +import com.zjkl.vectorgraphrag.model.Triplet; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Extracts knowledge triplets (subject-predicate-object) from text using LLM. + */ +@Slf4j +public class TripletExtractor { + + private static final String SYSTEM_PROMPT = + "You are an expert knowledge graph builder. Your task is to extract knowledge triplets from the given text.\n" + + "\n" + + "A triplet consists of:\n" + + "- Subject: An entity (person, place, thing, concept, etc.)\n" + + "- Predicate: The relationship between subject and object\n" + + "- Object: Another entity\n" + + "\n" + + "Guidelines:\n" + + "1. Extract all meaningful relationships from the text\n" + + "2. Keep entities concise but complete\n" + + "3. Use clear, specific predicates\n" + + "4. Extract both explicit and implicit relationships\n" + + "5. Ensure triplets are factually accurate based on the text\n" + + "\n" + + "Return your response as a JSON object with a \"triplets\" array, where each triplet is an array of [subject, predicate, object]."; + + private static final String EXAMPLE_INPUT = + "Text: Albert Einstein was born in Ulm, Germany in 1879. He developed the theory of relativity, which revolutionized physics. Einstein worked at the Institute for Advanced Study in Princeton."; + + private static final String EXAMPLE_OUTPUT = + "{\"triplets\": [[\"Albert Einstein\", \"was born in\", \"Ulm, Germany\"], [\"Albert Einstein\", \"was born in\", \"1879\"], [\"Albert Einstein\", \"developed\", \"the theory of relativity\"], [\"the theory of relativity\", \"revolutionized\", \"physics\"], [\"Albert Einstein\", \"worked at\", \"the Institute for Advanced Study\"], [\"the Institute for Advanced Study\", \"is located in\", \"Princeton\"]]}"; + + private final OpenAiClient openAiClient; + private final Gson gson; + + public TripletExtractor(VectorGraphRagSettings settings, OpenAiClient openAiClient) { + this.openAiClient = openAiClient; + this.gson = new Gson(); + } + + public List extract(String text) { + if (text == null || text.trim().isEmpty()) return List.of(); + + try { + List> examples = List.of( + Map.of("user", EXAMPLE_INPUT, "assistant", EXAMPLE_OUTPUT) + ); + String response = openAiClient.chat(SYSTEM_PROMPT, examples, "Text: " + text); + return parseResponse(response); + } catch (Exception e) { + log.warn("Triplet extraction failed: {}", e.getMessage()); + return List.of(); + } + } + + public List extractFromDocuments(List documents, boolean showProgress) { + for (int i = 0; i < documents.size(); i++) { + Document doc = documents.get(i); + List triplets = extract(doc.getText()); + doc.setTriplets(triplets); + // Store as raw arrays in metadata for backward compat + List> rawTriplets = triplets.stream() + .map(t -> List.of(t.getSubject(), t.getPredicate(), t.getObject())) + .collect(Collectors.toList()); + doc.getMetadata().put("triplets", rawTriplets); + if (showProgress) { + log.info("Extracted triplets for doc {}/{}: {} triplets", i + 1, documents.size(), triplets.size()); + } + } + return documents; + } + + private List parseResponse(String response) { + try { + JsonObject json = gson.fromJson(response, JsonObject.class); + JsonArray tripletsArray = json.getAsJsonArray("triplets"); + if (tripletsArray == null) return List.of(); + + List results = new ArrayList<>(); + for (int i = 0; i < tripletsArray.size(); i++) { + JsonArray arr = tripletsArray.get(i).getAsJsonArray(); + if (arr.size() >= 3) { + String subject = arr.get(0).getAsString().trim(); + String predicate = arr.get(1).getAsString().trim(); + String object = arr.get(2).getAsString().trim(); + if (!subject.isEmpty() && !predicate.isEmpty() && !object.isEmpty()) { + results.add(new Triplet(subject, predicate, object)); + } + } + } + return results; + } catch (Exception e) { + log.warn("Failed to parse triplet response: {}", e.getMessage()); + return List.of(); + } + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Document.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Document.java new file mode 100644 index 0000000..16cc3a3 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Document.java @@ -0,0 +1,24 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Document { + private String id; + private String text; + @Builder.Default + private Map metadata = new HashMap<>(); + @Builder.Default + private List triplets = new ArrayList<>(); +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Entity.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Entity.java new file mode 100644 index 0000000..245ab5c --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Entity.java @@ -0,0 +1,31 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Objects; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Entity { + private String id; + private String name; + private List embedding; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Entity entity)) return false; + return name.equalsIgnoreCase(entity.name); + } + + @Override + public int hashCode() { + return Objects.hash(name.toLowerCase()); + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/EvictionResult.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/EvictionResult.java new file mode 100644 index 0000000..69337e2 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/EvictionResult.java @@ -0,0 +1,19 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EvictionResult { + @Builder.Default + private boolean occurred = false; + @Builder.Default + private int beforeCount = 0; + @Builder.Default + private int afterCount = 0; +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/ExtractionResult.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/ExtractionResult.java new file mode 100644 index 0000000..ee53dc9 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/ExtractionResult.java @@ -0,0 +1,28 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExtractionResult { + @Builder.Default + private List documents = new ArrayList<>(); + @Builder.Default + private List entities = new ArrayList<>(); + @Builder.Default + private List relations = new ArrayList<>(); + @Builder.Default + private Map> entityToRelationIds = new HashMap<>(); + @Builder.Default + private Map> relationToPassageIds = new HashMap<>(); +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Passage.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Passage.java new file mode 100644 index 0000000..f66d08c --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Passage.java @@ -0,0 +1,23 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Passage { + private String id; + private String text; + @Builder.Default + private List entityIds = new ArrayList<>(); + @Builder.Default + private List relationIds = new ArrayList<>(); + private List embedding; +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/QueryResult.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/QueryResult.java new file mode 100644 index 0000000..ab96290 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/QueryResult.java @@ -0,0 +1,34 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QueryResult { + private String query; + private String answer; + @Builder.Default + private List queryEntities = new ArrayList<>(); + @Builder.Default + private List retrievedPassages = new ArrayList<>(); + @Builder.Default + private List retrievedRelations = new ArrayList<>(); + @Builder.Default + private List expandedRelations = new ArrayList<>(); + @Builder.Default + private List rerankedRelations = new ArrayList<>(); + private Object subgraph; + @Builder.Default + private List passages = new ArrayList<>(); + private RetrievalDetail retrievalDetail; + private RerankResult rerankResult; + private EvictionResult evictionResult; +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Relation.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Relation.java new file mode 100644 index 0000000..e01fcbc --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Relation.java @@ -0,0 +1,22 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Relation { + private String id; + private String text; + private Triplet triplet; + @Builder.Default + private List sourcePassageIds = new ArrayList<>(); + private List embedding; +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RerankResult.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RerankResult.java new file mode 100644 index 0000000..d896b0b --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RerankResult.java @@ -0,0 +1,20 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RerankResult { + @Builder.Default + private List selectedRelationIds = new ArrayList<>(); + @Builder.Default + private List selectedRelationTexts = new ArrayList<>(); +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RetrievalDetail.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RetrievalDetail.java new file mode 100644 index 0000000..e523aa5 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RetrievalDetail.java @@ -0,0 +1,28 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RetrievalDetail { + @Builder.Default + private List entityIds = new ArrayList<>(); + @Builder.Default + private List entityTexts = new ArrayList<>(); + @Builder.Default + private List entityScores = new ArrayList<>(); + @Builder.Default + private List relationIds = new ArrayList<>(); + @Builder.Default + private List relationTexts = new ArrayList<>(); + @Builder.Default + private List relationScores = new ArrayList<>(); +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RetrievalResult.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RetrievalResult.java new file mode 100644 index 0000000..6f143f7 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/RetrievalResult.java @@ -0,0 +1,43 @@ +package com.zjkl.vectorgraphrag.model; + +import com.zjkl.vectorgraphrag.graph.SubGraph; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RetrievalResult { + @Builder.Default + private List entityIds = new ArrayList<>(); + @Builder.Default + private List entityTexts = new ArrayList<>(); + @Builder.Default + private List entityScores = new ArrayList<>(); + @Builder.Default + private List relationIds = new ArrayList<>(); + @Builder.Default + private List relationTexts = new ArrayList<>(); + @Builder.Default + private List relationScores = new ArrayList<>(); + private SubGraph subgraph; + @Builder.Default + private List expandedRelationIds = new ArrayList<>(); + @Builder.Default + private List expandedRelationTexts = new ArrayList<>(); + private String query; + @Builder.Default + private List queryEntities = new ArrayList<>(); + @Builder.Default + private boolean evictionOccurred = false; + @Builder.Default + private int evictionBeforeCount = 0; + @Builder.Default + private int evictionAfterCount = 0; +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Triplet.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Triplet.java new file mode 100644 index 0000000..ff9cbfc --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/model/Triplet.java @@ -0,0 +1,40 @@ +package com.zjkl.vectorgraphrag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Triplet { + private String subject; + private String predicate; + private String object; + + public String toRelationText() { + return subject + " " + predicate + " " + object; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Triplet triplet)) return false; + return subject.equalsIgnoreCase(triplet.subject) + && predicate.equalsIgnoreCase(triplet.predicate) + && object.equalsIgnoreCase(triplet.object); + } + + @Override + public int hashCode() { + return Objects.hash( + subject.toLowerCase(), + predicate.toLowerCase(), + object.toLowerCase() + ); + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/storage/EmbeddingClient.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/storage/EmbeddingClient.java new file mode 100644 index 0000000..b933930 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/storage/EmbeddingClient.java @@ -0,0 +1,92 @@ +package com.zjkl.vectorgraphrag.storage; + +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import com.zjkl.vectorgraphrag.llm.OpenAiClient; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unified embedding model wrapper. + * Supports OpenAI embedding models. + */ +@Slf4j +public class EmbeddingClient { + + private final VectorGraphRagSettings settings; + private final OpenAiClient openAiClient; + private Integer dimension; + + public EmbeddingClient(VectorGraphRagSettings settings, OpenAiClient openAiClient) { + this.settings = settings; + this.openAiClient = openAiClient; + } + + public int getDimension() { + if (dimension == null) { + List test = embed("test"); + dimension = test.size(); + } + return dimension; + } + + public List embed(String text) { + List raw = openAiClient.embed(text); + return l2Normalize(raw); + } + + public List> embedBatch(List texts) { + return embedBatch(texts, false); + } + + public List> embedBatch(List texts, boolean showProgress) { + List> results = new ArrayList<>(); + for (int i = 0; i < texts.size(); i += settings.getBatchSize()) { + int end = Math.min(i + settings.getBatchSize(), texts.size()); + List batch = texts.subList(i, end); + try { + // Use batch API: all texts in one HTTP call + List> batchResults = openAiClient.embedBatch(batch); + for (List vec : batchResults) { + results.add(vec != null ? l2Normalize(vec) : fallbackVector()); + } + } catch (Exception e) { + log.warn("Batch embedding failed, falling back to single: {}", e.getMessage()); + for (String text : batch) { + try { + results.add(embed(text)); + } catch (Exception e2) { + log.warn("Embedding failed for text, using zero vector: {}", e2.getMessage()); + results.add(fallbackVector()); + } + } + } + if (showProgress) { + log.info("Embedded {}/{} texts", end, texts.size()); + } + } + return results; + } + + private List fallbackVector() { + List zero = new ArrayList<>(); + for (int j = 0; j < getDimension(); j++) zero.add(0.0f); + return zero; + } + + private List l2Normalize(List vector) { + double sum = 0.0; + for (float v : vector) { + sum += v * v; + } + double norm = Math.sqrt(sum); + if (norm == 0) return vector; + + List normalized = new ArrayList<>(vector.size()); + for (float v : vector) { + normalized.add((float) (v / norm)); + } + return normalized; + } +} diff --git a/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/storage/MilvusStore.java b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/storage/MilvusStore.java new file mode 100644 index 0000000..e3a0399 --- /dev/null +++ b/VectorGraphRag/src/main/java/com/zjkl/vectorgraphrag/storage/MilvusStore.java @@ -0,0 +1,621 @@ +package com.zjkl.vectorgraphrag.storage; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.zjkl.vectorgraphrag.config.VectorGraphRagSettings; +import io.milvus.v2.client.MilvusClientV2; +import io.milvus.v2.client.ConnectConfig; +import io.milvus.v2.common.ConsistencyLevel; +import io.milvus.v2.common.DataType; +import io.milvus.v2.common.IndexParam; +import io.milvus.v2.service.vector.request.data.BaseVector; +import io.milvus.v2.service.vector.request.data.FloatVec; +import io.milvus.v2.service.collection.request.CreateCollectionReq; +import io.milvus.v2.service.collection.request.DropCollectionReq; +import io.milvus.v2.service.collection.request.HasCollectionReq; +import io.milvus.v2.service.index.request.CreateIndexReq; +import io.milvus.v2.service.vector.request.InsertReq; +import io.milvus.v2.service.vector.request.QueryReq; +import io.milvus.v2.service.vector.request.SearchReq; +import io.milvus.v2.service.vector.request.UpsertReq; +import io.milvus.v2.service.vector.response.InsertResp; +import io.milvus.v2.service.vector.response.QueryResp; +import io.milvus.v2.service.vector.response.SearchResp; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Milvus vector store for entities, relations, and passages. + * + * Manages three collections: + * - Entity collection: entities with embeddings and adjacency metadata + * - Relation collection: relations with embeddings and triplet fields + * - Passage collection: passages with embeddings + */ +@Slf4j +public class MilvusStore { + + private final VectorGraphRagSettings settings; + private final EmbeddingClient embeddingClient; + private final MilvusClientV2 client; + private final Gson gson; + + private final String entityCollection; + private final String relationCollection; + private final String passageCollection; + + public MilvusStore(VectorGraphRagSettings settings, EmbeddingClient embeddingClient) { + this.settings = settings; + this.embeddingClient = embeddingClient; + this.gson = new Gson(); + + ConnectConfig.ConnectConfigBuilder builder = ConnectConfig.builder() + .uri(settings.getMilvusUri()); + if (settings.getMilvusToken() != null && !settings.getMilvusToken().isEmpty()) { + builder.token(settings.getMilvusToken()); + } + if (settings.getMilvusDb() != null && !settings.getMilvusDb().isEmpty()) { + builder.dbName(settings.getMilvusDb()); + } + this.client = new MilvusClientV2(builder.build()); + + String prefix = settings.hasCollectionPrefix() ? settings.getCollectionPrefix() + "_" : ""; + this.entityCollection = prefix + settings.getEntityCollection(); + this.relationCollection = prefix + settings.getRelationCollection(); + this.passageCollection = prefix + settings.getPassageCollection(); + } + + // ==================== Collection Management ==================== + + public void createCollections(boolean dropExisting) { + for (String name : List.of(entityCollection, relationCollection, passageCollection)) { + createCollection(name, dropExisting); + } + } + + public void dropCollections() { + for (String name : List.of(entityCollection, relationCollection, passageCollection)) { + try { + if (client.hasCollection(HasCollectionReq.builder().collectionName(name).build())) { + client.dropCollection(DropCollectionReq.builder().collectionName(name).build()); + log.info("Dropped collection: {}", name); + } + } catch (Exception e) { + log.warn("Error dropping collection {}: {}", name, e.getMessage()); + } + } + } + + private void createCollection(String collectionName, boolean dropExisting) { + try { + if (client.hasCollection(HasCollectionReq.builder().collectionName(collectionName).build())) { + if (dropExisting) { + client.dropCollection(DropCollectionReq.builder().collectionName(collectionName).build()); + } else { + return; + } + } + } catch (Exception e) { + log.warn("Error checking collection {}: {}", collectionName, e.getMessage()); + } + + int dim = settings.getEmbeddingDimension() > 0 + ? settings.getEmbeddingDimension() + : embeddingClient.getDimension(); + + CreateCollectionReq.CollectionSchema schema = CreateCollectionReq.CollectionSchema.builder().build(); + schema.addField(io.milvus.v2.service.collection.request.AddFieldReq.builder() + .fieldName("id") + .dataType(DataType.VarChar) + .maxLength(64) + .isPrimaryKey(true) + .autoID(false) + .build()); + schema.addField(io.milvus.v2.service.collection.request.AddFieldReq.builder() + .fieldName("vector") + .dataType(DataType.FloatVector) + .dimension(dim) + .build()); + schema.addField(io.milvus.v2.service.collection.request.AddFieldReq.builder() + .fieldName("text") + .dataType(DataType.VarChar) + .maxLength(65535) + .build()); + // enableDynamicField is true by default + + client.createCollection(CreateCollectionReq.builder() + .collectionName(collectionName) + .collectionSchema(schema) + .consistencyLevel(ConsistencyLevel.fromName(settings.getMilvusConsistencyLevel())) + .build()); + + // Create index + IndexParam.IndexParamBuilder paramBuilder = IndexParam.builder() + .fieldName("vector") + .indexType(IndexParam.IndexType.valueOf(settings.getMilvusIndexType())) + .metricType(IndexParam.MetricType.valueOf(settings.getMilvusMetricType())); + if (settings.getMilvusIndexParams() != null && !settings.getMilvusIndexParams().isEmpty()) { + paramBuilder.indexName("vector_index"); + paramBuilder.extraParams(settings.getMilvusIndexParams()); + } + client.createIndex(CreateIndexReq.builder() + .collectionName(collectionName) + .indexParams(List.of(paramBuilder.build())) + .build()); + + log.info("Created collection: {} (dim={})", collectionName, dim); + } + + // ==================== Generic Insert ==================== + + private List insertData(String collectionName, List ids, List texts, + List> embeddings, List> metadatas, + boolean showProgress) { + if (ids == null || ids.isEmpty()) return List.of(); + + int batchSize = settings.getBatchSize(); + List resultIds = new ArrayList<>(); + + for (int i = 0; i < ids.size(); i += batchSize) { + int end = Math.min(i + batchSize, ids.size()); + List batch = new ArrayList<>(); + + for (int j = i; j < end; j++) { + JsonObject row = new JsonObject(); + row.addProperty("id", ids.get(j)); + row.addProperty("text", texts.get(j)); + + JsonArray vectorArr = new JsonArray(); + List vec = embeddings.get(j); + for (float v : vec) vectorArr.add(v); + row.add("vector", vectorArr); + + if (metadatas != null && j < metadatas.size() && metadatas.get(j) != null) { + Map meta = metadatas.get(j); + for (Map.Entry entry : meta.entrySet()) { + if (entry.getValue() instanceof List) { + JsonArray arr = new JsonArray(); + for (Object item : (List) entry.getValue()) { + arr.add(item.toString()); + } + row.add(entry.getKey(), arr); + } else if (entry.getValue() instanceof String) { + row.addProperty(entry.getKey(), (String) entry.getValue()); + } else if (entry.getValue() instanceof Number) { + row.addProperty(entry.getKey(), (Number) entry.getValue()); + } else if (entry.getValue() instanceof Boolean) { + row.addProperty(entry.getKey(), (Boolean) entry.getValue()); + } + } + } + + batch.add(row); + resultIds.add(ids.get(j)); + } + + InsertReq insertReq = InsertReq.builder() + .collectionName(collectionName) + .data(batch) + .build(); + client.insert(insertReq); + + if (showProgress) { + log.info("Inserted {}/{} into {}", end, ids.size(), collectionName); + } + } + + return resultIds; + } + + // ==================== Entity Operations ==================== + + public List insertEntities(List entityTexts, List ids, + List> embeddings, List> metadatas, + boolean showProgress) { + if (entityTexts == null || entityTexts.isEmpty()) return List.of(); + + List> embeds = embeddings != null ? embeddings + : embeddingClient.embedBatch(entityTexts, showProgress); + List finalIds = ids != null ? ids : entityTexts.stream() + .map(t -> UUID.randomUUID().toString()).collect(Collectors.toList()); + + return insertData(entityCollection, finalIds, entityTexts, embeds, metadatas, showProgress); + } + + public List> searchEntities(List> queryEmbeddings, int topK) { + List floatVecs = queryEmbeddings.stream() + .map(FloatVec::new) + .collect(Collectors.toList()); + SearchReq searchReq = SearchReq.builder() + .collectionName(entityCollection) + .data(floatVecs) + .topK(topK) + .outputFields(List.of("id", "text", "relation_ids", "passage_ids")) + .build(); + + SearchResp resp = client.search(searchReq); + return convertSearchResults(resp); + } + + public List> searchRelations(List queryEmbedding, int topK) { + SearchReq searchReq = SearchReq.builder() + .collectionName(relationCollection) + .data(Collections.singletonList(new FloatVec(queryEmbedding))) + .topK(topK) + .outputFields(List.of("id", "text", "entity_ids", "passage_ids", "subject", "predicate", "object")) + .build(); + + SearchResp resp = client.search(searchReq); + return convertSearchResults(resp); + } + + public List> searchPassages(List queryEmbedding, int topK, String filter) { + SearchReq.SearchReqBuilder builder = SearchReq.builder() + .collectionName(passageCollection) + .data(Collections.singletonList(new FloatVec(queryEmbedding))) + .topK(topK) + .outputFields(List.of("id", "text", "entity_ids", "relation_ids")); + if (filter != null && !filter.isEmpty()) { + builder.filter(filter); + } + + SearchResp resp = client.search(builder.build()); + return convertSearchResults(resp); + } + + // ==================== Relation Operations ==================== + + public List insertRelations(List relationTexts, List ids, + List> embeddings, List> metadatas, + boolean showProgress) { + if (relationTexts == null || relationTexts.isEmpty()) return List.of(); + + List> embeds = embeddings != null ? embeddings + : embeddingClient.embedBatch(relationTexts, showProgress); + List finalIds = ids != null ? ids : relationTexts.stream() + .map(t -> UUID.randomUUID().toString()).collect(Collectors.toList()); + + return insertData(relationCollection, finalIds, relationTexts, embeds, metadatas, showProgress); + } + + // ==================== Passage Operations ==================== + + public List insertPassages(List passageTexts, List ids, + List> embeddings, List> metadatas, + boolean showProgress) { + if (passageTexts == null || passageTexts.isEmpty()) return List.of(); + + List> embeds = embeddings != null ? embeddings + : embeddingClient.embedBatch(passageTexts, showProgress); + List finalIds = ids != null ? ids : passageTexts.stream() + .map(t -> UUID.randomUUID().toString()).collect(Collectors.toList()); + + return insertData(passageCollection, finalIds, passageTexts, embeds, metadatas, showProgress); + } + + // ==================== Query by ID ==================== + + public List> getEntitiesByIds(List entityIds) { + if (entityIds == null || entityIds.isEmpty()) return List.of(); + + String idsStr = entityIds.stream() + .map(id -> "\"" + id + "\"") + .collect(Collectors.joining(", ")); + QueryResp resp = client.query(QueryReq.builder() + .collectionName(entityCollection) + .filter("id in [" + idsStr + "]") + .outputFields(List.of("id", "text", "relation_ids", "passage_ids")) + .build()); + return convertQueryResults(resp); + } + + public List> getRelationsByIds(List relationIds) { + if (relationIds == null || relationIds.isEmpty()) return List.of(); + + String idsStr = relationIds.stream() + .map(id -> "\"" + id + "\"") + .collect(Collectors.joining(", ")); + QueryResp resp = client.query(QueryReq.builder() + .collectionName(relationCollection) + .filter("id in [" + idsStr + "]") + .outputFields(List.of("id", "text", "entity_ids", "passage_ids", "subject", "predicate", "object")) + .build()); + return convertQueryResults(resp); + } + + public List> getPassagesByIds(List passageIds) { + return getPassagesByIds(passageIds, null); + } + + public List> getPassagesByIds(List passageIds, String filter) { + if (passageIds == null || passageIds.isEmpty()) return List.of(); + + String idsStr = passageIds.stream() + .map(id -> "\"" + id + "\"") + .collect(Collectors.joining(", ")); + String filterExpr = "id in [" + idsStr + "]"; + if (filter != null && !filter.isEmpty()) { + filterExpr = "(" + filterExpr + ") and (" + filter + ")"; + } + + QueryResp resp = client.query(QueryReq.builder() + .collectionName(passageCollection) + .filter(filterExpr) + .outputFields(List.of("id", "text", "entity_ids", "relation_ids")) + .build()); + return convertQueryResults(resp); + } + + public List queryPassageIds(String filter) { + if (filter == null || filter.trim().isEmpty()) return List.of(); + + QueryResp resp = client.query(QueryReq.builder() + .collectionName(passageCollection) + .filter(filter) + .outputFields(List.of("id")) + .build()); + return resp.getQueryResults().stream() + .map(r -> r.getEntity().get("id").toString()) + .collect(Collectors.toList()); + } + + // ==================== Update / Upsert ==================== + + public boolean upsertEntity(String entityId, String text, List embedding, + List relationIds, List passageIds) { + List> existing = getEntitiesByIds(List.of(entityId)); + if (existing.isEmpty()) return false; + + Map data = existing.get(0); + JsonObject update = new JsonObject(); + update.addProperty("id", entityId); + update.addProperty("text", text != null ? text : (String) data.get("text")); + + if (embedding != null) { + JsonArray arr = new JsonArray(); + for (float v : embedding) arr.add(v); + update.add("vector", arr); + } else { + List newEmbed = embeddingClient.embed(update.get("text").getAsString()); + JsonArray arr = new JsonArray(); + for (float v : newEmbed) arr.add(v); + update.add("vector", arr); + } + + if (relationIds != null) { + JsonArray arr = new JsonArray(); + for (String id : relationIds) arr.add(id); + update.add("relation_ids", arr); + } + if (passageIds != null) { + JsonArray arr = new JsonArray(); + for (String id : passageIds) arr.add(id); + update.add("passage_ids", arr); + } + + client.upsert(UpsertReq.builder().collectionName(entityCollection).data(List.of(update)).build()); + return true; + } + + public boolean upsertRelation(String relationId, String text, List embedding, + List entityIds, List passageIds, + String subject, String predicate, String object) { + List> existing = getRelationsByIds(List.of(relationId)); + if (existing.isEmpty()) return false; + + Map data = existing.get(0); + JsonObject update = new JsonObject(); + update.addProperty("id", relationId); + update.addProperty("text", text != null ? text : (String) data.get("text")); + + String finalText = update.get("text").getAsString(); + if (embedding != null) { + JsonArray arr = new JsonArray(); + for (float v : embedding) arr.add(v); + update.add("vector", arr); + } else { + List newEmbed = embeddingClient.embed(finalText); + JsonArray arr = new JsonArray(); + for (float v : newEmbed) arr.add(v); + update.add("vector", arr); + } + + if (entityIds != null) { + JsonArray arr = new JsonArray(); + for (String id : entityIds) arr.add(id); + update.add("entity_ids", arr); + } + if (passageIds != null) { + JsonArray arr = new JsonArray(); + for (String id : passageIds) arr.add(id); + update.add("passage_ids", arr); + } + if (subject != null) update.addProperty("subject", subject); + if (predicate != null) update.addProperty("predicate", predicate); + if (object != null) update.addProperty("object", object); + + client.upsert(UpsertReq.builder().collectionName(relationCollection).data(List.of(update)).build()); + return true; + } + + public boolean upsertPassage(String passageId, String text, List embedding, + List entityIds, List relationIds) { + List> existing = getPassagesByIds(List.of(passageId)); + if (existing.isEmpty()) return false; + + Map data = existing.get(0); + JsonObject update = new JsonObject(); + update.addProperty("id", passageId); + update.addProperty("text", text != null ? text : (String) data.get("text")); + + String finalText = update.get("text").getAsString(); + if (embedding != null) { + JsonArray arr = new JsonArray(); + for (float v : embedding) arr.add(v); + update.add("vector", arr); + } else { + List newEmbed = embeddingClient.embed(finalText); + JsonArray arr = new JsonArray(); + for (float v : newEmbed) arr.add(v); + update.add("vector", arr); + } + + if (entityIds != null) { + JsonArray arr = new JsonArray(); + for (String id : entityIds) arr.add(id); + update.add("entity_ids", arr); + } + if (relationIds != null) { + JsonArray arr = new JsonArray(); + for (String id : relationIds) arr.add(id); + update.add("relation_ids", arr); + } + + client.upsert(UpsertReq.builder().collectionName(passageCollection).data(List.of(update)).build()); + return true; + } + + // ==================== Delete Operations ==================== + + public boolean deleteEntity(String entityId) { + return deleteById(entityCollection, entityId); + } + + public boolean deleteRelation(String relationId) { + return deleteById(relationCollection, relationId); + } + + public boolean deletePassage(String passageId) { + return deleteById(passageCollection, passageId); + } + + public int deleteEntities(List entityIds) { + return deleteByIds(entityCollection, entityIds); + } + + public int deleteRelations(List relationIds) { + return deleteByIds(relationCollection, relationIds); + } + + public int deletePassages(List passageIds) { + return deleteByIds(passageCollection, passageIds); + } + + private boolean deleteById(String collection, String id) { + try { + client.delete(io.milvus.v2.service.vector.request.DeleteReq.builder() + .collectionName(collection) + .filter("id == \"" + id + "\"") + .build()); + return true; + } catch (Exception e) { + log.warn("Delete failed for {} in {}: {}", id, collection, e.getMessage()); + return false; + } + } + + private int deleteByIds(String collection, List ids) { + if (ids == null || ids.isEmpty()) return 0; + String idsStr = ids.stream().map(id -> "\"" + id + "\"").collect(Collectors.joining(", ")); + client.delete(io.milvus.v2.service.vector.request.DeleteReq.builder() + .collectionName(collection) + .filter("id in [" + idsStr + "]") + .build()); + return ids.size(); + } + + // ==================== Utility ==================== + + public Map getCollectionStats() { + Map stats = new HashMap<>(); + stats.put("entityCount", 0); + stats.put("relationCount", 0); + stats.put("passageCount", 0); + + try { + // MilvusClientV2 doesn't have getCollectionStats directly; + // We'll do a rough count by querying all IDs + } catch (Exception e) { + log.warn("Error getting collection stats: {}", e.getMessage()); + } + return stats; + } + + /** + * Search relations with a custom filter expression (for eviction strategy). + */ + public List> searchRelationsWithFilter(List queryEmbedding, + int topK, String filter) { + SearchReq.SearchReqBuilder builder = SearchReq.builder() + .collectionName(relationCollection) + .data(Collections.singletonList(new FloatVec(queryEmbedding))) + .topK(topK) + .outputFields(List.of("id", "text")); + if (filter != null && !filter.isEmpty()) { + builder.filter(filter); + } + + SearchResp resp = client.search(builder.build()); + return convertSearchResults(resp); + } + + public MilvusClientV2 getClient() { + return client; + } + + public String getRelationCollection() { + return relationCollection; + } + + // ==================== Result Conversion ==================== + + private List> convertSearchResults(SearchResp resp) { + List> results = new ArrayList<>(); + if (resp.getSearchResults() == null) return results; + + for (List resultList : resp.getSearchResults()) { + for (SearchResp.SearchResult r : resultList) { + Map map = new HashMap<>(); + map.put("id", r.getId()); + map.put("distance", r.getScore()); + + Map entity = r.getEntity(); + Map flat = new HashMap<>(); + if (entity != null) { + for (Map.Entry e : entity.entrySet()) { + flat.put(e.getKey(), e.getValue()); + } + } + map.put("entity", flat); + results.add(map); + } + } + return results; + } + + private List> convertQueryResults(QueryResp resp) { + List> results = new ArrayList<>(); + if (resp.getQueryResults() == null) return results; + + for (QueryResp.QueryResult r : resp.getQueryResults()) { + Map map = new HashMap<>(); + Map entity = r.getEntity(); + if (entity != null) { + map.putAll(entity); + } + results.add(map); + } + return results; + } + + public static String combineFilters(String... filters) { + return Arrays.stream(filters) + .filter(f -> f != null && !f.trim().isEmpty()) + .map(String::trim) + .map(f -> "(" + f + ")") + .collect(Collectors.joining(" and ")); + } +} diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/VectorGraphRAG.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/VectorGraphRAG.class new file mode 100644 index 0000000..8b3f7d3 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/VectorGraphRAG.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings$VectorGraphRagSettingsBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings$VectorGraphRagSettingsBuilder.class new file mode 100644 index 0000000..c725961 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings$VectorGraphRagSettingsBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings.class new file mode 100644 index 0000000..6f445a3 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/config/VectorGraphRagSettings.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/Graph.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/Graph.class new file mode 100644 index 0000000..887b836 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/Graph.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/GraphBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/GraphBuilder.class new file mode 100644 index 0000000..827c4af Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/GraphBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/GraphRetriever.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/GraphRetriever.class new file mode 100644 index 0000000..ef249ec Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/GraphRetriever.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraph.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraph.class new file mode 100644 index 0000000..8b7f576 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraph.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphEntity$SubGraphEntityBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphEntity$SubGraphEntityBuilder.class new file mode 100644 index 0000000..35d533a Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphEntity$SubGraphEntityBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphEntity.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphEntity.class new file mode 100644 index 0000000..0c12fec Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphEntity.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphPassage$SubGraphPassageBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphPassage$SubGraphPassageBuilder.class new file mode 100644 index 0000000..fc4e814 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphPassage$SubGraphPassageBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphPassage.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphPassage.class new file mode 100644 index 0000000..86533f6 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphPassage.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphRelation$SubGraphRelationBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphRelation$SubGraphRelationBuilder.class new file mode 100644 index 0000000..f1517c9 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphRelation$SubGraphRelationBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphRelation.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphRelation.class new file mode 100644 index 0000000..0961b5d Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/graph/SubGraphRelation.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/AnswerGenerator.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/AnswerGenerator.class new file mode 100644 index 0000000..d7bd413 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/AnswerGenerator.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/EntityExtractor.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/EntityExtractor.class new file mode 100644 index 0000000..7fd8974 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/EntityExtractor.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/LLMCache.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/LLMCache.class new file mode 100644 index 0000000..b6fcbe1 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/LLMCache.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/LLMReranker.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/LLMReranker.class new file mode 100644 index 0000000..a7954b0 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/LLMReranker.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/OpenAiClient.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/OpenAiClient.class new file mode 100644 index 0000000..fb84b9b Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/OpenAiClient.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/TripletExtractor.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/TripletExtractor.class new file mode 100644 index 0000000..c5ad06c Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/llm/TripletExtractor.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Document$DocumentBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Document$DocumentBuilder.class new file mode 100644 index 0000000..7d53fde Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Document$DocumentBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Document.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Document.class new file mode 100644 index 0000000..86509c2 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Document.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Entity$EntityBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Entity$EntityBuilder.class new file mode 100644 index 0000000..088ad09 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Entity$EntityBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Entity.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Entity.class new file mode 100644 index 0000000..484bd65 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Entity.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/EvictionResult$EvictionResultBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/EvictionResult$EvictionResultBuilder.class new file mode 100644 index 0000000..301be01 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/EvictionResult$EvictionResultBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/EvictionResult.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/EvictionResult.class new file mode 100644 index 0000000..1a33ded Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/EvictionResult.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/ExtractionResult$ExtractionResultBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/ExtractionResult$ExtractionResultBuilder.class new file mode 100644 index 0000000..a4b46e6 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/ExtractionResult$ExtractionResultBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/ExtractionResult.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/ExtractionResult.class new file mode 100644 index 0000000..b4c752e Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/ExtractionResult.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Passage$PassageBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Passage$PassageBuilder.class new file mode 100644 index 0000000..aa4e877 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Passage$PassageBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Passage.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Passage.class new file mode 100644 index 0000000..9fa621c Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Passage.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/QueryResult$QueryResultBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/QueryResult$QueryResultBuilder.class new file mode 100644 index 0000000..122c997 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/QueryResult$QueryResultBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/QueryResult.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/QueryResult.class new file mode 100644 index 0000000..4ee4c03 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/QueryResult.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Relation$RelationBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Relation$RelationBuilder.class new file mode 100644 index 0000000..6eb298d Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Relation$RelationBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Relation.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Relation.class new file mode 100644 index 0000000..b140feb Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Relation.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RerankResult$RerankResultBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RerankResult$RerankResultBuilder.class new file mode 100644 index 0000000..8b51762 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RerankResult$RerankResultBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RerankResult.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RerankResult.class new file mode 100644 index 0000000..a3bcf96 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RerankResult.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalDetail$RetrievalDetailBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalDetail$RetrievalDetailBuilder.class new file mode 100644 index 0000000..9cb3fac Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalDetail$RetrievalDetailBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalDetail.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalDetail.class new file mode 100644 index 0000000..c4edd76 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalDetail.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalResult$RetrievalResultBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalResult$RetrievalResultBuilder.class new file mode 100644 index 0000000..6110152 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalResult$RetrievalResultBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalResult.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalResult.class new file mode 100644 index 0000000..1640bb8 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/RetrievalResult.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Triplet$TripletBuilder.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Triplet$TripletBuilder.class new file mode 100644 index 0000000..b5c2f00 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Triplet$TripletBuilder.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Triplet.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Triplet.class new file mode 100644 index 0000000..e8f4193 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/model/Triplet.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/storage/EmbeddingClient.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/storage/EmbeddingClient.class new file mode 100644 index 0000000..c8ff89a Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/storage/EmbeddingClient.class differ diff --git a/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/storage/MilvusStore.class b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/storage/MilvusStore.class new file mode 100644 index 0000000..b8f2344 Binary files /dev/null and b/VectorGraphRag/target/classes/com/zjkl/vectorgraphrag/storage/MilvusStore.class differ diff --git a/VectorGraphRag/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/VectorGraphRag/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..56c64b5 --- /dev/null +++ b/VectorGraphRag/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,43 @@ +com\zjkl\vectorgraphrag\model\RetrievalResult.class +com\zjkl\vectorgraphrag\config\VectorGraphRagSettings.class +com\zjkl\vectorgraphrag\storage\MilvusStore.class +com\zjkl\vectorgraphrag\model\Document.class +com\zjkl\vectorgraphrag\model\Passage$PassageBuilder.class +com\zjkl\vectorgraphrag\model\ExtractionResult$ExtractionResultBuilder.class +com\zjkl\vectorgraphrag\config\VectorGraphRagSettings$VectorGraphRagSettingsBuilder.class +com\zjkl\vectorgraphrag\model\EvictionResult$EvictionResultBuilder.class +com\zjkl\vectorgraphrag\model\EvictionResult.class +com\zjkl\vectorgraphrag\model\QueryResult$QueryResultBuilder.class +com\zjkl\vectorgraphrag\model\RetrievalDetail.class +com\zjkl\vectorgraphrag\graph\SubGraphEntity$SubGraphEntityBuilder.class +com\zjkl\vectorgraphrag\model\RerankResult.class +com\zjkl\vectorgraphrag\llm\LLMReranker.class +com\zjkl\vectorgraphrag\storage\EmbeddingClient.class +com\zjkl\vectorgraphrag\model\RerankResult$RerankResultBuilder.class +com\zjkl\vectorgraphrag\graph\GraphRetriever.class +com\zjkl\vectorgraphrag\graph\SubGraphRelation.class +com\zjkl\vectorgraphrag\model\RetrievalResult$RetrievalResultBuilder.class +com\zjkl\vectorgraphrag\model\ExtractionResult.class +com\zjkl\vectorgraphrag\model\QueryResult.class +com\zjkl\vectorgraphrag\graph\SubGraph.class +com\zjkl\vectorgraphrag\model\Entity$EntityBuilder.class +com\zjkl\vectorgraphrag\model\Passage.class +com\zjkl\vectorgraphrag\model\Triplet.class +com\zjkl\vectorgraphrag\model\Entity.class +com\zjkl\vectorgraphrag\graph\SubGraphPassage.class +com\zjkl\vectorgraphrag\model\Relation$RelationBuilder.class +com\zjkl\vectorgraphrag\VectorGraphRAG.class +com\zjkl\vectorgraphrag\llm\TripletExtractor.class +com\zjkl\vectorgraphrag\graph\SubGraphRelation$SubGraphRelationBuilder.class +com\zjkl\vectorgraphrag\model\RetrievalDetail$RetrievalDetailBuilder.class +com\zjkl\vectorgraphrag\model\Document$DocumentBuilder.class +com\zjkl\vectorgraphrag\model\Triplet$TripletBuilder.class +com\zjkl\vectorgraphrag\model\Relation.class +com\zjkl\vectorgraphrag\graph\GraphBuilder.class +com\zjkl\vectorgraphrag\graph\SubGraphPassage$SubGraphPassageBuilder.class +com\zjkl\vectorgraphrag\graph\Graph.class +com\zjkl\vectorgraphrag\llm\AnswerGenerator.class +com\zjkl\vectorgraphrag\graph\SubGraphEntity.class +com\zjkl\vectorgraphrag\llm\LLMCache.class +com\zjkl\vectorgraphrag\llm\OpenAiClient.class +com\zjkl\vectorgraphrag\llm\EntityExtractor.class diff --git a/VectorGraphRag/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/VectorGraphRag/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..42bd9a8 --- /dev/null +++ b/VectorGraphRag/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,28 @@ +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\config\VectorGraphRagSettings.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\graph\Graph.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\graph\GraphBuilder.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\graph\GraphRetriever.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\graph\SubGraph.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\graph\SubGraphEntity.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\graph\SubGraphPassage.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\graph\SubGraphRelation.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\llm\AnswerGenerator.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\llm\EntityExtractor.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\llm\LLMCache.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\llm\LLMReranker.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\llm\OpenAiClient.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\llm\TripletExtractor.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\Document.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\Entity.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\EvictionResult.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\ExtractionResult.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\Passage.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\QueryResult.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\Relation.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\RerankResult.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\RetrievalDetail.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\RetrievalResult.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\model\Triplet.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\storage\EmbeddingClient.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\storage\MilvusStore.java +C:\Users\饶策\Desktop\langchain4j_sister_backend\VectorGraphRag\src\main\java\com\zjkl\vectorgraphrag\VectorGraphRAG.java