File size: 4,465 Bytes
09801ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""
Vector retrieval module - REAL implementation using FAISS
Retrieves relevant documents from user's vector store
"""

from vector.store_faiss import FaissStore
from core.llm import embed_text
import numpy as np


def retrieve(query: str, k: int = 5, user_id: str = None):
    """
    Retrieve relevant documents from FAISS vector store.
    IMPROVED: Ensures results come from ALL uploaded files, not just top-k similar.
    
    Args:
        query: Search query string
        k: Number of results to return (will return k per file for comprehensive coverage)
        user_id: User identifier for per-user store
        
    Returns:
        List of document dicts with text, source, and score
    """
    if not user_id:
        print("โš ๏ธ No user_id provided to retrieve()")
        return []
    
    try:
        # Load user's FAISS store
        store = FaissStore.load_or_create(user_id=user_id, fresh=False)
        
        if store.index.ntotal == 0:
            print(f"โš ๏ธ FAISS store for {user_id} is empty (0 vectors)")
            return []
        
        print(f"๐Ÿ“š Searching FAISS store with {store.index.ntotal} vectors for user {user_id}")
        
        # Embed the query
        query_embedding = embed_text(query)
        query_vector = np.array([query_embedding], dtype="float32")
        
        # IMPROVED: Search for MORE results then balance across files
        search_k = min(50, max(k * 5, store.index.ntotal))  # Search 50 or 5x requested
        distances, indices = store.index.search(query_vector, search_k)
        
        # Group results by source file
        results_by_source = {}
        all_results = []
        
        for i, (dist, idx) in enumerate(zip(distances[0], indices[0])):
            if idx < 0 or idx >= len(store.meta):
                continue
            
            meta = store.meta[idx]
            source = meta.get("source", "Unknown")
            
            # Calculate similarity score (convert L2 distance to similarity)
            score = 1 / (1 + dist)
            
            result = {
                "text": meta.get("text", ""),
                "source": source,
                "score": float(score),
                "chunk": meta.get("chunk", i),
                "metadata": meta
            }
            
            # Group by source
            if source not in results_by_source:
                results_by_source[source] = []
            results_by_source[source].append(result)
            all_results.append(result)
        
        # CRITICAL: Ensure we get results from EACH file
        num_files = len(results_by_source)
        print(f"๐Ÿ“ Found results from {num_files} files: {list(results_by_source.keys())}")
        
        if num_files > 1:
            # Balance results across all files
            chunks_per_file = max(2, k // num_files + 1)
            balanced_results = []
            
            for source, file_results in results_by_source.items():
                # Take top chunks from each file (sorted by score)
                file_results.sort(key=lambda x: x['score'], reverse=True)
                balanced_results.extend(file_results[:chunks_per_file])
                print(f"   ๐Ÿ“„ {source}: {len(file_results[:chunks_per_file])} chunks")
            
            # Sort balanced results by score for final ordering
            balanced_results.sort(key=lambda x: x['score'], reverse=True)
            results = balanced_results[:k * 2]  # Return more for comprehensive coverage
        else:
            # Single file - just return top k
            results = all_results[:k]
        
        print(f"โœ… Retrieved {len(results)} documents from FAISS (balanced across {num_files} files)")
        return results
        
    except Exception as e:
        print(f"โš ๏ธ Error in retrieve(): {e}")
        import traceback
        traceback.print_exc()
        return []


def hybrid_retrieve(query: str, k: int = 5, user_id: str = None):
    """
    Hybrid retrieval combining FAISS vector search with keyword matching.
    Falls back to standard retrieve if hybrid index not available.
    
    Args:
        query: Search query string
        k: Number of results to return
        user_id: User identifier
        
    Returns:
        List of document dicts
    """
    # For now, use standard FAISS retrieval
    # TODO: Implement BM25 keyword matching + score fusion
    return retrieve(query, k=k, user_id=user_id)