diff --git a/src/main/java/com/enterprise/iqk/retrieval/HybridRetrievalService.java b/src/main/java/com/enterprise/iqk/retrieval/HybridRetrievalService.java index a3e8e2f..bdcc48c 100644 --- a/src/main/java/com/enterprise/iqk/retrieval/HybridRetrievalService.java +++ b/src/main/java/com/enterprise/iqk/retrieval/HybridRetrievalService.java @@ -33,7 +33,52 @@ public class HybridRetrievalService { private static final double WEB_WEIGHT = 0.15; private static final long SOURCE_TIMEOUT_SECONDS = 3; + /** + * Retrieve documents using hybrid search with default source weights. + * Equivalent to calling {@link #retrieve(String, String, String, int, HybridWeights)} + * with default weights (vector=0.40, keyword=0.25, graph=0.20, web=0.15). + * + * @param query search query + * @param tenantId tenant identifier for filtering + * @param chatId chat/conversation identifier for filtering + * @param topK number of top results to return + * @return hybrid retrieval result with deduplicated, scored documents + */ public HybridRetrievalResult retrieve(String query, String tenantId, String chatId, int topK) { + return retrieve(query, tenantId, chatId, topK, HybridWeights.DEFAULT); + } + + /** + * Retrieve documents using hybrid search with configurable per-source weights. + * Each source runs in parallel via {@link CompletableFuture} and results are + * merged, deduplicated by content fingerprint, and sorted by final weighted score. + * + * Tuning tip: factual/definitional queries benefit from higher vector weight; + * exact-match/lookup queries benefit from higher keyword weight. + * + * @param query search query + * @param tenantId tenant identifier for filtering + * @param chatId chat/conversation identifier for filtering + * @param topK number of top results to return + * @param weights per-source weight configuration; weights are normalized to sum to 1.0 + * @return hybrid retrieval result with deduplicated, scored documents + */ + public HybridRetrievalResult retrieve(String query, String tenantId, String chatId, int topK, HybridWeights weights) { + + // Normalize weights to sum to 1.0 + HybridWeights normalized = weights.normalize(); + + Timer.Sample sample = Timer.start(meterRegistry); + String outcome = "error"; + try { + CompletableFuture> vectorFuture = retrieveAsync("vector", + () -> vectorRetriever.retrieve(query, tenantId, chatId), normalized.vectorWeight()); + CompletableFuture> keywordFuture = retrieveAsync("keyword", + () -> keywordRetriever.retrieve(query, tenantId, chatId, topK), normalized.keywordWeight()); + CompletableFuture> graphFuture = retrieveAsync("graph", + () -> graphRetriever.retrieve(query, tenantId, topK), normalized.graphWeight()); + CompletableFuture> webFuture = retrieveAsync("web", + () -> webRetriever.retrieve(query, topK), normalized.webWeight()); Timer.Sample sample = Timer.start(meterRegistry); String outcome = "error"; try { diff --git a/src/main/java/com/enterprise/iqk/retrieval/HybridWeights.java b/src/main/java/com/enterprise/iqk/retrieval/HybridWeights.java new file mode 100644 index 0000000..cb1110c --- /dev/null +++ b/src/main/java/com/enterprise/iqk/retrieval/HybridWeights.java @@ -0,0 +1,28 @@ +package com.enterprise.iqk.retrieval; + +/** + * Configurable per-source weights for hybrid retrieval. + */ +public record HybridWeights(double vectorWeight, double keywordWeight, + double graphWeight, double webWeight) { + + public static final HybridWeights DEFAULT = new HybridWeights(0.40, 0.25, 0.20, 0.15); + public static final HybridWeights SEMANTIC = new HybridWeights(0.55, 0.20, 0.15, 0.10); + public static final HybridWeights KEYWORD = new HybridWeights(0.20, 0.50, 0.15, 0.15); + public static final HybridWeights BALANCED = new HybridWeights(0.25, 0.25, 0.25, 0.25); + + public static HybridWeights of(double vector, double keyword, double graph, double web) { + return new HybridWeights(vector, keyword, graph, web); + } + + public static HybridWeights semantic() { return SEMANTIC; } + public static HybridWeights keyword() { return KEYWORD; } + public static HybridWeights balanced() { return BALANCED; } + + public HybridWeights normalize() { + double sum = vectorWeight + keywordWeight + graphWeight + webWeight; + if (sum <= 0) return BALANCED; + if (Math.abs(sum - 1.0) < 1e-9) return this; + return new HybridWeights(vectorWeight / sum, keywordWeight / sum, graphWeight / sum, webWeight / sum); + } +}