File size: 7,689 Bytes
8f436f8
bc17620
fd1c27c
8f436f8
3b14745
 
8533b77
29b0b66
 
 
8655cf3
29b0b66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438d4f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd1c27c
 
 
438d4f9
 
 
 
29b0b66
 
68c9fc6
438d4f9
fd1c27c
68c9fc6
fd1c27c
 
b2dd427
fd1c27c
b2dd427
 
 
fd1c27c
 
 
 
 
438d4f9
 
fd1c27c
 
 
 
 
 
 
 
 
 
438d4f9
 
 
 
 
68c9fc6
8f436f8
fd1c27c
29b0b66
bc17620
8f436f8
 
 
29b0b66
bc17620
8f436f8
 
 
 
 
 
 
29b0b66
bc17620
8f436f8
 
29b0b66
bc17620
acf0656
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import streamlit as st
from config import sparse_index as indexB
from config import *
import nltk
import zlib
import base64
import json
import hashlib
import uuid

nltk.download('punkt_tab')

# Initialiser l'état de session pour Streamlit
if "bm25_corpus" not in st.session_state:
    st.session_state.bm25_corpus = []
if "indexing_done" not in st.session_state:
    st.session_state.indexing_done = False

def normalize_text(text):
    """Normalise le texte en supprimant les espaces superflus et les sauts de ligne."""
    return " ".join(text.split())

def get_text_hash(text):
    """Retourne un hash du texte normalisé."""
    normalized_text = normalize_text(text)
    return hashlib.md5(normalized_text.encode("utf-8")).hexdigest()

def generate_unique_id():
    """Génère un ID unique."""
    return str(uuid.uuid4())

def get_existing_vectors(index):
    """Récupère les hashs des textes déjà indexés dans Pinecone."""
    existing_hashes = set()
    try:
        results = index.query(vector=[0] * 1024, top_k=10000, include_metadata=True)
        for match in results.get("matches", []):
            if "metadata" in match and "compressed_text" in match["metadata"]:
                compressed_text = match["metadata"]["compressed_text"]
                existing_hashes.add(get_text_hash(decompress_text(compressed_text)))
    except Exception as e:
        st.error(f"Erreur lors de la récupération des vecteurs existants : {e}")
    return existing_hashes

def index_pdf_B(texts):
    """Indexe les textes en évitant les doublons."""
    if not texts:
        st.error("La liste des textes ne peut pas être vide.")
        return

    # Vérifier si l'indexation a déjà été effectuée
    if st.session_state.indexing_done:
        st.warning("L'indexation a déjà été effectuée. Ignorer.")
        return

    st.write("Indexation en cours, veuillez patienter...")

    # Récupérer les textes déjà indexés
    existing_hashes = get_existing_vectors(indexB)

    # Initialiser BM25
    st.session_state.bm25_corpus = texts  
    sparse_encoder.fit(texts)

    for i, text in enumerate(texts):
        chunks = split_text_into_chunks(text, max_chunk_size=1024)
        for j, chunk in enumerate(chunks):
            # Normaliser le chunk et calculer son hash
            normalized_chunk = normalize_text(chunk)
            chunk_hash = get_text_hash(normalized_chunk)

            # Vérifier si ce texte est déjà indexé
            if chunk_hash in existing_hashes:
                continue  # Ignorer ce document car il est déjà indexé

            # Générer les valeurs sparse avec BM25
            sparse_values = sparse_encoder.encode_documents([chunk])[0]

            # Sérialiser les valeurs sparse en JSON
            sparse_values_json = json.dumps(sparse_values)

            # Créer les métadonnées avec le texte compressé et les valeurs sparse sérialisées
            metadata = {
                "compressed_text": compress_text(normalized_chunk),
                "sparse_values": sparse_values_json  # Utiliser la version sérialisée
            }

            # Vérifier la taille des métadonnées
            metadata_size = get_metadata_size(metadata)
            if metadata_size > 40960:  # 40 KB
                print(f"Attention : la taille des métadonnées ({metadata_size} bytes) dépasse la limite de 40960 bytes.")
                continue

            # Générer le vecteur dense avec SentenceTransformer
            vector = model.encode([chunk]).tolist()[0]

            # Générer un ID unique pour le vecteur
            vector_id = generate_unique_id()

            # Indexer le vecteur avec les métadonnées
            indexB.upsert([(vector_id, vector, metadata)])

            # Ajouter le hash du texte à la liste des textes indexés
            existing_hashes.add(chunk_hash)

    st.session_state.indexing_done = True  # Marquer l'indexation comme terminée
    st.success("Indexation terminée sans duplication de contenu.")

def hybrid_search(query, alpha, k, similarity_threshold):
    """Récupère les documents pertinents en combinant les résultats de Pinecone et BM25."""
    try:
        # Générer le vecteur dense pour la requête
        query_vector = model.encode([query]).tolist()[0]
        
        # Générer les valeurs sparse pour la requête
        sparse_query = sparse_encoder.encode_queries([query])[0]
        
        # Effectuer une recherche hybride dans l'indexB
        results = indexB.query(
            vector=query_vector,  # Vecteur dense
            sparse_vector=sparse_query,  # Valeurs sparse
            top_k=k,  # Nombre de résultats à retourner
            include_metadata=True,  # Inclure les métadonnées
            alpha=alpha  
        )
        
        # Récupérer les documents pertinents
        relevant_docs = []
        total_words = 0
        total_tokens = 0

        for match in results.get("matches", []):
            if "metadata" in match and "compressed_text" in match["metadata"]:
                score = match.get("score", 0)  # Score de similarité
                if score >= similarity_threshold:  # Filtrer par seuil
                    compressed_text = match["metadata"]["compressed_text"]
                    sparse_values_json = match["metadata"].get("sparse_values")

                    # Désérialiser les valeurs sparse si elles existent
                    sparse_values = json.loads(sparse_values_json) if sparse_values_json else None

                    # Décompression du texte
                    text = decompress_text(compressed_text)
                    relevant_docs.append({
                        "text": text,
                        "sparse_values": sparse_values,
                        "score": score
                    })

                    # Calcul du nombre de mots et de tokens
                    total_words += len(text.split())  # Nombre de mots (séparés par des espaces)
                    total_tokens += len(model.tokenizer.encode(text))  # Nombre de tokens

            else:
                print(f"Skipping match due to missing metadata or compressed_text: {match}")

        # Calcul des moyennes
        num_docs = len(relevant_docs)
        avg_words_per_doc = total_words / num_docs if num_docs > 0 else 0
        avg_tokens_per_doc = total_tokens / num_docs if num_docs > 0 else 0

        print(f"Nombre de documents récupérés : {num_docs}")
        print(f"Moyenne de mots par document : {avg_words_per_doc:.2f}")
        print(f"Moyenne de tokens par document : {avg_tokens_per_doc:.2f}")

        return relevant_docs

    except Exception as e:
        st.error(f"Erreur lors de la recherche hybride : {e}")
        return []



def compress_text(text):
        """Compresse un texte en base64."""
        compressed = zlib.compress(text.encode("utf-8"))
        return base64.b64encode(compressed).decode("utf-8")

def decompress_text(compressed_text):
        """Décompresse un texte compressé en base64."""
        try:
            compressed_data = base64.b64decode(compressed_text.encode("utf-8"))
            return zlib.decompress(compressed_data).decode("utf-8")
        except Exception as e:
            st.error(f"Erreur de décompression : {e}")
            return ""

def split_text_into_chunks(text, max_chunk_size=1024):
        """Divise un texte en morceaux de taille maximale `max_chunk_size`."""
        return [text[i:i+max_chunk_size] for i in range(0, len(text), max_chunk_size)]

def get_metadata_size(metadata):
        """Calcule la taille des métadonnées en octets."""
        return len(str(metadata).encode("utf-8"))