tuliodisanto commited on
Commit
361e3fc
·
verified ·
1 Parent(s): 20080a9

Delete enhanced_search_v2.py

Browse files
Files changed (1) hide show
  1. enhanced_search_v2.py +0 -375
enhanced_search_v2.py DELETED
@@ -1,375 +0,0 @@
1
- # enhanced_search_v2.py (Versão Final, Corrigida e Comentada)
2
- ###################################################################################################
3
- #
4
- # Este arquivo contém o motor de busca principal.
5
- #
6
- # O fluxo de dados da consulta foi corrigido para garantir que a correção ortográfica
7
- # seja propagada para todas as camadas da busca (lexical e semântica), restaurando a
8
- # funcionalidade original e mantendo as melhorias de ranking.
9
- #
10
- # A ordenação final usa a hierarquia:
11
- # 1. Feedback do Usuário
12
- # 2. "Golden Match" (Score Semântico de 100%)
13
- # 3. Score Híbrido (IA + Texto)
14
- # 4. Cobertura do Rol (desempate)
15
- #
16
- ###################################################################################################
17
-
18
- import pandas as pd
19
- import re
20
- from thefuzz import process, fuzz
21
- from unidecode import unidecode
22
- import time
23
- from sentence_transformers import util
24
- import torch
25
- import math
26
- from collections import defaultdict
27
- from rank_bm25 import BM25Okapi
28
-
29
- # --- FUNÇÕES AUXILIARES DE NORMALIZAÇÃO --- #
30
-
31
- def literal_normalize_text(text):
32
- """Normalização "agressiva" para a Camada 0 (Busca Literal)."""
33
- if pd.isna(text): return ""
34
- normalized = unidecode(str(text).lower())
35
- normalized = re.sub(r'[^\w\s]', ' ', normalized)
36
- return re.sub(r'\s+', ' ', normalized).strip()
37
-
38
- def normalize_text(text):
39
- """Normalização padrão (minúsculas, sem acentos)."""
40
- if pd.isna(text): return ""
41
- return unidecode(str(text).lower().strip())
42
-
43
- def get_longest_word(query_text):
44
- """Extrai a palavra mais longa de uma consulta como último recurso."""
45
- words = re.findall(r'\b\w{4,}\b', query_text)
46
- if not words: return ""
47
- return max(words, key=len)
48
-
49
-
50
- # --- FUNÇÕES DE FORMATAÇÃO E DESTAQUE --- #
51
-
52
- def format_result(row_data, row_index, match_type="", score=0):
53
- """Converte uma linha do DataFrame em um dicionário de resultado padronizado."""
54
- data = row_data.copy()
55
- is_rol = data.get('Correlacao_Rol', '').strip().lower() == 'sim'
56
-
57
- if not is_rol:
58
- data['Grupo'], data['Subgrupo'], data['Vigencia'], data['Resolucao_Normativa'] = '', '', '', ''
59
- data['PAC'], data['DUT'] = '---', '---'
60
- else:
61
- data['PAC'] = 'Sim' if data.get('PAC', '').strip().lower() == 'pac' else 'Não'
62
- original_dut_value = data.get('DUT', '').strip()
63
- if original_dut_value and original_dut_value.replace('.', '', 1).isdigit():
64
- data['DUT'] = f'Sim, DUT nº {original_dut_value}'
65
- else: data['DUT'] = 'Não'
66
-
67
- standard_columns = [
68
- 'Codigo_TUSS', 'Descricao_TUSS', 'Correlacao_Rol', 'Procedimento_Rol',
69
- 'Resolucao_Normativa', 'Vigencia', 'OD', 'AMB', 'HCO', 'HSO', 'PAC',
70
- 'DUT', 'SUBGRUPO', 'GRUPO', 'CAPITULO', 'Sinonimo_1', 'Sinonimo_2',
71
- 'Sinonimo_3', 'Sinonimo_4', 'Semantico'
72
- ]
73
- formatted_data = {col: data.get(col, '') for col in standard_columns}
74
-
75
- result = {
76
- "row_index": row_index,
77
- "score": round(score),
78
- "text_score": round(score),
79
- "semantic_score": 0,
80
- "match_type": match_type,
81
- "is_rol_procedure": is_rol
82
- }
83
- result.update(formatted_data)
84
- return result
85
-
86
- def _highlight_matches(results, query):
87
- """Adiciona tags <b> para destacar os termos da busca nos resultados."""
88
- if not query or not results: return results
89
- stopwords = {'de', 'do', 'da', 'dos', 'das', 'a', 'o', 'e', 'em', 'um', 'uma', 'para', 'com'}
90
- query_words = {word for word in normalize_text(query).split() if len(word) > 2 and word not in stopwords}
91
- cols_to_highlight = ['Descricao_TUSS', 'Procedimento_Rol', 'Sinonimo_1', 'Sinonimo_2', 'Sinonimo_3', 'Sinonimo_4', 'Semantico']
92
- for result in results:
93
- for col in cols_to_highlight:
94
- original_text = result.get(col, '')
95
- if original_text and query_words:
96
- highlighted_text = original_text
97
- for word in sorted(list(query_words), key=len, reverse=True):
98
- pattern = r'\b(' + re.escape(word) + r')\b'
99
- highlighted_text = re.sub(pattern, r'<b>\1</b>', highlighted_text, flags=re.IGNORECASE)
100
- result[f"{col}_highlighted"] = highlighted_text
101
- else:
102
- result[f"{col}_highlighted"] = original_text
103
- return results
104
-
105
-
106
- # --- FUNÇÕES DE CARREGAMENTO DE DADOS --- #
107
-
108
- def load_and_prepare_database(db_path):
109
- """Carrega e pré-processa a base de dados principal para otimizar a busca."""
110
- try:
111
- print(f"Carregando e preparando a base de dados de: {db_path}...")
112
- df_original = pd.read_csv(db_path, dtype=str).fillna('')
113
- search_cols = ['Descricao_TUSS', 'Procedimento_Rol', 'Sinonimo_1', 'Sinonimo_2', 'Sinonimo_3', 'Sinonimo_4', 'Semantico', 'SUBGRUPO', 'GRUPO', 'CAPITULO']
114
- df_normalized = df_original.copy()
115
- for col in search_cols + ['Codigo_TUSS']:
116
- if col in df_normalized.columns:
117
- df_normalized[f'{col}_literal'] = df_normalized[col].apply(literal_normalize_text)
118
- df_normalized[f'{col}_norm'] = df_normalized[col].apply(normalize_text)
119
-
120
- df_normalized['full_text_norm'] = df_normalized[[f'{col}_norm' for col in search_cols if f'{col}_norm' in df_normalized.columns]].agg(' '.join, axis=1)
121
-
122
- print("Criando dicionário da base, modelo BM25...")
123
- tokenized_corpus = [text.split() for text in df_normalized['full_text_norm']]
124
- db_word_set = {word for doc in tokenized_corpus for word in doc if word}
125
- bm25_model = BM25Okapi(tokenized_corpus)
126
-
127
- print("Criando corpus para busca fuzzy...")
128
- fuzzy_search_corpus = []
129
- for index, row in df_normalized.iterrows():
130
- for col in ['Descricao_TUSS', 'Procedimento_Rol', 'Sinonimo_1', 'Sinonimo_2', 'Sinonimo_3', 'Sinonimo_4']:
131
- if f'{col}_norm' in row and pd.notna(row[f'{col}_norm']):
132
- val = row[f'{col}_norm']
133
- if val: fuzzy_search_corpus.append((val, index, f'{col}_norm'))
134
-
135
- print(f"Base de dados pronta com {len(df_original)} procedimentos.")
136
- return df_original, df_normalized, fuzzy_search_corpus, bm25_model, db_word_set, None, None
137
- except Exception as e: print(f"Erro crítico ao carregar/preparar a base de dados: {e}"); raise
138
-
139
- def load_general_dictionary(path):
140
- """Carrega um dicionário geral de palavras em português."""
141
- try:
142
- with open(path, 'r', encoding='utf-8') as f: words = {normalize_text(line.strip()) for line in f if line.strip()}
143
- return words
144
- except (FileNotFoundError, Exception): return set()
145
-
146
- def load_correction_corpus(dict_path, column_name='Termo_Correto'):
147
- """Carrega um corpus de correções ortográficas de um CSV."""
148
- try:
149
- df_dict = pd.read_csv(dict_path, dtype=str).fillna('')
150
- if column_name not in df_dict.columns: return [], []
151
- original_corpus = df_dict[column_name].dropna().astype(str).tolist()
152
- normalized_corpus = [normalize_text(term) for term in original_corpus]
153
- return original_corpus, normalized_corpus
154
- except (FileNotFoundError, Exception): return [], []
155
-
156
-
157
- # --- FUNÇÕES DE RECLASSIFICAÇÃO SEMÂNTICA (IA) --- #
158
-
159
- def create_unified_document_text(result_dict):
160
- """Cria uma string de texto única para a análise da IA."""
161
- text_parts = {
162
- result_dict.get('Descricao_TUSS', ''), result_dict.get('Procedimento_Rol', ''),
163
- result_dict.get('Semantico', ''), result_dict.get('SUBGRUPO', ''),
164
- result_dict.get('GRUPO', ''), result_dict.get('CAPITULO', '')
165
- }
166
- for i in range(1, 5): text_parts.add(result_dict.get(f'Sinonimo_{i}', ''))
167
- return ". ".join(sorted([part for part in text_parts if part and str(part).strip()]))
168
-
169
- def rerank_with_cross_encoder(query, results_list, model):
170
- """Reclassifica resultados usando um Cross-Encoder e a lógica de Score Híbrido."""
171
- if not model or not results_list or not query: return results_list, "Cross-Encoder não fornecido ou lista de candidatos vazia."
172
- sentence_pairs = [[query, create_unified_document_text(result)] for result in results_list]
173
- if not sentence_pairs: return results_list, "Não foram encontrados pares para reordenar."
174
-
175
- try:
176
- raw_scores = model.predict(sentence_pairs, show_progress_bar=False)
177
- semantic_scores_normalized = torch.sigmoid(torch.tensor(raw_scores)).numpy() * 100
178
-
179
- for i, result in enumerate(results_list):
180
- semantic_score = round(semantic_scores_normalized[i])
181
- text_score = result.get('text_score', 0)
182
-
183
- result['semantic_score'] = semantic_score
184
- result['match_type'] = "Relevância Híbrida (IA+Texto)"
185
-
186
- if semantic_score >= 99.5:
187
- result['is_golden_match'] = True
188
- result['hybrid_score'] = semantic_score
189
- else:
190
- result['is_golden_match'] = False
191
- weights = (0.8, 0.2) if semantic_score >= 90 else (0.6, 0.4) if semantic_score >= 70 else (0.4, 0.6)
192
- result['hybrid_score'] = (semantic_score * weights[0]) + (text_score * weights[1])
193
-
194
- key_function = lambda x: (x.get('is_golden_match', False), x.get('hybrid_score', 0), x.get('is_rol_procedure', False))
195
- reranked_results = sorted(results_list, key=key_function, reverse=True)
196
-
197
- log_message = "Reordenação final por: 1º Golden Match, 2º Score Híbrido, 3º Cobertura do Rol."
198
- return reranked_results, log_message
199
-
200
- except Exception as e:
201
- log_message = f"Erro no Cross-Encoder: {e}"; print(log_message)
202
- return results_list, log_message
203
-
204
-
205
- # --- FUNÇÃO INTERNA DE BUSCA COM CAMADAS --- #
206
-
207
- def _run_search_layers(literal_query, normalized_query, response, df_original, df_normalized, fuzzy_search_corpus, bm25_model, limit_per_layer):
208
- """Executa a busca em múltiplas camadas para encontrar candidatos."""
209
- matched_indices = set()
210
- stopwords = {'de', 'do', 'da', 'dos', 'das', 'a', 'o', 'e', 'em', 'um', 'uma', 'para', 'com'}
211
- query_words = [word for word in normalized_query.split() if word not in stopwords and len(word) > 1]
212
-
213
- def sort_key(x):
214
- return (x.get('score', 0), x.get('is_rol_procedure', False))
215
-
216
- for layer in ["literal_matches", "exact_matches", "logical_matches", "almost_exact_matches", "term_matches", "keyword_matches"]:
217
- response["results_by_layer"][layer] = []
218
-
219
- # Camadas 0 e 1 (Exatas)
220
- if literal_query:
221
- for col in ['Codigo_TUSS_literal', 'Descricao_TUSS_literal', 'Procedimento_Rol_literal']:
222
- matches = df_normalized[df_normalized[col] == literal_query]
223
- for index, _ in matches.iterrows():
224
- if index not in matched_indices:
225
- response["results_by_layer"]["literal_matches"].append(format_result(df_original.loc[index], index, "Texto Exato", 100))
226
- matched_indices.add(index)
227
-
228
- if normalized_query:
229
- matches = df_normalized[df_normalized['Codigo_TUSS_norm'] == normalized_query]
230
- for index, _ in matches.iterrows():
231
- if index not in matched_indices:
232
- response["results_by_layer"]["exact_matches"].append(format_result(df_original.loc[index], index, "Código Exato", 100))
233
- matched_indices.add(index)
234
- for col in ['Descricao_TUSS_norm', 'Procedimento_Rol_norm']:
235
- matches = df_normalized[df_normalized[col] == normalized_query]
236
- for index, _ in matches.iterrows():
237
- if index not in matched_indices:
238
- response["results_by_layer"]["exact_matches"].append(format_result(df_original.loc[index], index, "Exato (Normalizado)", 100))
239
- matched_indices.add(index)
240
-
241
- # Camada 2 (Lógica)
242
- if query_words:
243
- mask = pd.Series(True, index=df_normalized.index)
244
- for word in query_words: mask &= df_normalized['full_text_norm'].str.contains(r'\b' + re.escape(word) + r'\b', na=False)
245
- for index, row in df_normalized[mask & ~df_normalized.index.isin(matched_indices)].iterrows():
246
- score = fuzz.WRatio(normalized_query, row.get('full_text_norm', ''))
247
- response["results_by_layer"]["logical_matches"].append(format_result(df_original.loc[index], index, "Busca Lógica", score))
248
- matched_indices.add(index)
249
-
250
- # Camada 3 (Fuzzy)
251
- if fuzzy_search_corpus:
252
- processed_indices = set()
253
- for match_text, score in process.extractBests(normalized_query, [item[0] for item in fuzzy_search_corpus], scorer=fuzz.token_set_ratio, limit=limit_per_layer * 3, score_cutoff=90):
254
- if score == 100 and match_text == normalized_query: continue
255
- for _, original_index, _ in [item for item in fuzzy_search_corpus if item[0] == match_text]:
256
- if original_index not in matched_indices and original_index not in processed_indices:
257
- response["results_by_layer"]["almost_exact_matches"].append(format_result(df_original.loc[original_index], original_index, "Busca por Aproximação", 98))
258
- matched_indices.add(original_index); processed_indices.add(original_index)
259
-
260
- # Camada 4 (BM25)
261
- if query_words and bm25_model:
262
- doc_scores = bm25_model.get_scores(query_words)
263
- max_score = max(doc_scores) if any(doc_scores) else 1.0
264
- top_indices = sorted(range(len(doc_scores)), key=lambda i: doc_scores[i], reverse=True)[:limit_per_layer * 5]
265
- for i in top_indices:
266
- if doc_scores[i] > 0 and (original_index := df_normalized.index[i]) not in matched_indices:
267
- normalized_score = (doc_scores[i] / max_score) * 90
268
- response["results_by_layer"]["term_matches"].append(format_result(df_original.loc[original_index], original_index, "Relevância de Termos", normalized_score))
269
- matched_indices.add(original_index)
270
-
271
- # Ordena todas as camadas
272
- for layer in response["results_by_layer"]:
273
- response["results_by_layer"][layer] = sorted(response["results_by_layer"][layer], key=sort_key, reverse=True)[:limit_per_layer * 4]
274
-
275
- return None
276
-
277
-
278
- # --- FUNÇÃO PRINCIPAL QUE ORQUESTRA A BUSCA --- #
279
-
280
- def search_procedure_with_log(query, df_original, df_normalized, fuzzy_search_corpus, correction_corpus,
281
- portuguese_word_set, bm25_model, db_word_set,
282
- doc_freq, tuss_to_full_text_map,
283
- limit_per_layer=10,
284
- semantic_model=None,
285
- cross_encoder_model=None,
286
- user_best_matches_counts=None, user_feedback_threshold=10):
287
- """Orquestra todo o processo de busca, da correção à reordenação final."""
288
- RERANK_LIMIT = 50; start_time = time.time(); original_query = str(query).strip()
289
- response = {"search_log": [], "results_by_layer": {}, "final_semantic_results": [], "was_corrected": False, "original_query": original_query, "corrected_query": ""}
290
- if not original_query: response["search_log"].append("Query vazia."); return response
291
- response["search_log"].append(f"Buscando por: '{original_query}'")
292
-
293
- # ETAPA 1: CORREÇÃO ORTOGRÁFICA
294
- stopwords = {'de', 'do', 'da', 'dos', 'das', 'a', 'o', 'e', 'em', 'um', 'uma', 'para', 'com'}; query_after_correction = original_query
295
- original_correction_corpus, normalized_correction_corpus = correction_corpus; valid_words = portuguese_word_set.union(db_word_set)
296
- if valid_words and original_correction_corpus:
297
- words, corrected_words, made_correction = query_after_correction.split(), [], False
298
- for word in words:
299
- norm_word = normalize_text(word); clean_norm_word = re.sub(r'[^\w]', '', norm_word)
300
- if len(norm_word) < 4 or norm_word in stopwords or clean_norm_word in valid_words: corrected_words.append(word); continue
301
- match_norm, score = process.extractOne(clean_norm_word, normalized_correction_corpus, scorer=fuzz.token_set_ratio)
302
- if score >= 85:
303
- corrected_word = original_correction_corpus[normalized_correction_corpus.index(match_norm)]
304
- if word.istitle(): corrected_word = corrected_word.title()
305
- elif word.isupper(): corrected_word = corrected_word.upper()
306
- corrected_words.append(corrected_word); made_correction = True
307
- else: corrected_words.append(word)
308
- if made_correction: query_after_correction = " ".join(corrected_words); response.update({"was_corrected": True, "corrected_query": query_after_correction}); response["search_log"].append(f"Query corrigida para: '{query_after_correction}'.")
309
-
310
- # <<< CORREÇÃO DO FLUXO DE DADOS >>>
311
- # ETAPA 2: PREPARAÇÃO DAS QUERIES - Restaurado para a lógica original e correta.
312
- # Cria uma versão da query para busca lexical (sem stopwords) e mantém a completa para a IA.
313
- cleaned_query = " ".join([word for word in query_after_correction.split() if normalize_text(word) not in stopwords])
314
- normalized_query = normalize_text(cleaned_query) # Para buscas por palavras-chave (BM25, Lógica, etc.)
315
- if not cleaned_query.strip(): response["search_log"].append("Query resultante vazia."); return response
316
- if cleaned_query != query_after_correction: response["search_log"].append(f"Query limpa (sem stop words): '{cleaned_query}'")
317
-
318
- # ETAPA 3: EXECUÇÃO DA BUSCA
319
- _run_search_layers(literal_normalize_text(query_after_correction), normalized_query, response, df_original, df_normalized, fuzzy_search_corpus, bm25_model, limit_per_layer)
320
-
321
- # ETAPA 3.5: LÓGICA DE EARLY EXIT
322
- high_confidence_results = response["results_by_layer"].get("literal_matches", []) + response["results_by_layer"].get("exact_matches", [])
323
- if high_confidence_results:
324
- response["search_log"].append("\n--- [MODO DE ALTA CONFIANÇA - SAÍDA ANTECIPADA] ---")
325
- final_list = sorted(high_confidence_results, key=lambda x: (x.get('text_score', 0), x.get('is_rol_procedure', False)), reverse=True)
326
- query_for_highlight = query_after_correction
327
- response["final_semantic_results"] = _highlight_matches(final_list[:15], query_for_highlight)
328
- end_time = time.time(); response["search_duration_seconds"] = round(end_time - start_time, 4)
329
- response["search_log"].append(f"Busca completa (saída antecipada) em {response['search_duration_seconds']} segundos.")
330
- print(f"\n\n==================== LOG DE DEPURAÇÃO (QUERY: '{original_query}') ====================")
331
- for log_item in response["search_log"]: print(log_item)
332
- return response
333
-
334
- # ETAPA 4: AGREGAÇÃO HIERÁRQUICA E REORDENAÇÃO
335
- all_candidates = []
336
- # Camadas são agregadas em ordem de prioridade
337
- for layer in ["logical_matches", "almost_exact_matches", "term_matches", "keyword_matches"]:
338
- all_candidates.extend(response["results_by_layer"].get(layer, []))
339
-
340
- unique_candidates = list({r['row_index']: r for r in all_candidates}.values())
341
- response["search_log"].append(f"\n--- [MODO DE BUSCA AMPLA] ---")
342
- response["search_log"].append(f"Total de candidatos únicos (após desduplicação): {len(unique_candidates)}.")
343
-
344
- if user_best_matches_counts:
345
- query_norm_fb = normalize_text(response.get("corrected_query") or original_query)
346
- for r in unique_candidates:
347
- votes = user_best_matches_counts.get(query_norm_fb, {}).get(r['Codigo_TUSS'], 0)
348
- if votes >= user_feedback_threshold: r.update({'is_user_best_match': True, 'feedback_votes': votes})
349
-
350
- response["search_log"].append(f"\n--- Análise e Reordenação ---")
351
-
352
- final_list = []
353
- if unique_candidates:
354
- # A IA recebe a consulta COMPLETA (com stopwords) para melhor contexto.
355
- query_for_semantic = query_after_correction
356
-
357
- prioritized_by_feedback = sorted([r for r in unique_candidates if r.get('is_user_best_match')], key=lambda x: (x.get('feedback_votes', 0), x.get('semantic_score', 0), x.get('text_score', 0)), reverse=True)
358
- to_rerank = [r for r in unique_candidates if not r.get('is_user_best_match')]
359
-
360
- final_list.extend(prioritized_by_feedback)
361
- if prioritized_by_feedback: response["search_log"].append(f"{len(prioritized_by_feedback)} resultado(s) priorizado(s) por feedback.")
362
-
363
- if to_rerank:
364
- to_rerank_sorted = sorted(to_rerank, key=lambda x: x.get('text_score', 0), reverse=True)
365
- reranked_by_ia, log_msg = rerank_with_cross_encoder(query_for_semantic, to_rerank_sorted[:RERANK_LIMIT], cross_encoder_model)
366
- response["search_log"].append(log_msg)
367
- final_list.extend(reranked_by_ia)
368
-
369
- query_for_highlight = query_after_correction
370
- response["final_semantic_results"] = _highlight_matches(final_list[:15], query_for_highlight)
371
- end_time = time.time(); response["search_duration_seconds"] = round(end_time - start_time, 4)
372
- response["search_log"].append(f"Busca completa em {response['search_duration_seconds']} segundos.")
373
- print(f"\n\n==================== LOG DE DEPURAÇÃO (QUERY: '{original_query}') ====================")
374
- for log_item in response["search_log"]: print(log_item)
375
- return response