|
|
|
|
|
import json |
|
import re |
|
import numpy as np |
|
import faiss |
|
from collections import defaultdict |
|
from typing import List, Dict, Any, Tuple, Optional, Callable |
|
|
|
|
|
VEHICLE_TYPE_MAP: Dict[str, List[str]] = { |
|
"xe máy": ["xe máy", "xe mô tô", "xe gắn máy", "xe máy điện", "mô tô hai bánh", "mô tô ba bánh"], |
|
"ô tô": ["xe ô tô", "ô tô con", "ô tô tải", "ô tô khách", "xe con", "xe tải", "xe khách", "ô tô điện"], |
|
"xe cơ giới": ["xe cơ giới"], |
|
"xe thô sơ": ["xe thô sơ", "xe đạp", "xích lô", "xe đạp điện"], |
|
"người đi bộ": ["người đi bộ"], |
|
|
|
} |
|
|
|
|
|
|
|
def get_standardized_vehicle_type(text_input: Optional[str]) -> Optional[str]: |
|
""" |
|
Suy luận và chuẩn hóa loại phương tiện từ một chuỗi text. |
|
Ưu tiên các loại cụ thể trước, sau đó đến các loại chung hơn. |
|
""" |
|
if not text_input or not isinstance(text_input, str): |
|
return None |
|
|
|
text_lower = text_input.lower() |
|
|
|
|
|
|
|
is_moto = any(re.search(r'\b' + re.escape(kw) + r'\b', text_lower) for kw in VEHICLE_TYPE_MAP["xe máy"]) |
|
if is_moto: |
|
|
|
|
|
|
|
if "chuyên dùng" in text_lower and "xe máy chuyên dùng" not in text_lower: |
|
|
|
|
|
pass |
|
else: |
|
return "xe máy" |
|
|
|
|
|
is_car = any(re.search(r'\b' + re.escape(kw) + r'\b', text_lower) for kw in VEHICLE_TYPE_MAP["ô tô"]) |
|
if is_car: |
|
return "ô tô" |
|
|
|
|
|
|
|
|
|
for standard_type, keywords in VEHICLE_TYPE_MAP.items(): |
|
if standard_type in ["xe máy", "ô tô"]: |
|
continue |
|
if any(re.search(r'\b' + re.escape(kw) + r'\b', text_lower) for kw in keywords): |
|
return standard_type |
|
|
|
return None |
|
|
|
def tokenize_vi_for_bm25_setup(text: str) -> List[str]: |
|
""" |
|
Tokenize tiếng Việt đơn giản cho BM25: lowercase, loại bỏ dấu câu, split theo khoảng trắng. |
|
""" |
|
text = text.lower() |
|
text = re.sub(r'[^\w\s]', '', text) |
|
return text.split() |
|
|
|
|
|
def process_law_data_to_chunks(structured_data_input: Any) -> List[Dict[str, Any]]: |
|
""" |
|
Xử lý dữ liệu luật có cấu trúc (từ JSON) thành một danh sách phẳng các "chunks". |
|
Mỗi chunk bao gồm "text" và "metadata". |
|
""" |
|
flat_list: List[Dict[str, Any]] = [] |
|
|
|
if isinstance(structured_data_input, dict) and "article" in structured_data_input: |
|
articles_list: List[Dict[str, Any]] = [structured_data_input] |
|
elif isinstance(structured_data_input, list): |
|
articles_list = structured_data_input |
|
else: |
|
print("Lỗi: Dữ liệu đầu vào không phải là danh sách các Điều luật hoặc một đối tượng Điều luật.") |
|
return flat_list |
|
|
|
for article_data in articles_list: |
|
if not isinstance(article_data, dict): |
|
|
|
continue |
|
|
|
raw_article_title = article_data.get("article_title", "") |
|
article_metadata_base = { |
|
"source_document": article_data.get("source_document"), |
|
"article": article_data.get("article"), |
|
"article_title": raw_article_title |
|
} |
|
|
|
article_level_vehicle_type = get_standardized_vehicle_type(raw_article_title) or "không xác định" |
|
|
|
clauses = article_data.get("clauses", []) |
|
if not isinstance(clauses, list): |
|
|
|
continue |
|
|
|
for clause_data in clauses: |
|
if not isinstance(clause_data, dict): |
|
|
|
continue |
|
|
|
clause_metadata_base = article_metadata_base.copy() |
|
clause_number = clause_data.get("clause_number") |
|
clause_metadata_base.update({"clause_number": clause_number}) |
|
|
|
clause_summary_data = clause_data.get("clause_metadata_summary") |
|
if isinstance(clause_summary_data, dict): |
|
clause_metadata_base["overall_fine_note_for_clause_text"] = clause_summary_data.get("overall_fine_note_for_clause") |
|
clause_metadata_base["overall_points_deduction_note_for_clause_text"] = clause_summary_data.get("overall_points_deduction_note_for_clause") |
|
|
|
points_in_clause = clause_data.get("points_in_clause", []) |
|
if not isinstance(points_in_clause, list): |
|
|
|
continue |
|
|
|
if points_in_clause: |
|
for point_data in points_in_clause: |
|
if not isinstance(point_data, dict): |
|
|
|
continue |
|
|
|
chunk_text = point_data.get("point_text_original") |
|
if not chunk_text: chunk_text = point_data.get("violation_description_summary") |
|
if not chunk_text: continue |
|
|
|
current_chunk_metadata = clause_metadata_base.copy() |
|
current_chunk_metadata["point_id"] = point_data.get("point_id") |
|
current_chunk_metadata["violation_description_summary"] = point_data.get("violation_description_summary") |
|
|
|
|
|
current_chunk_metadata.update({ |
|
'has_fine': False, 'has_points_deduction': False, |
|
'has_license_suspension': False, 'has_confiscation': False |
|
}) |
|
penalty_types_for_this_point: List[str] = [] |
|
points_values: List[Any] = [] |
|
s_min_months: List[float] = [] |
|
s_max_months: List[float] = [] |
|
confiscation_items_list: List[str] = [] |
|
|
|
penalties = point_data.get("penalties_detail", []) |
|
if isinstance(penalties, list): |
|
for p_item in penalties: |
|
if not isinstance(p_item, dict): continue |
|
p_type_original, p_details = p_item.get("penalty_type"), p_item.get("details", {}) |
|
if p_type_original: penalty_types_for_this_point.append(str(p_type_original)) |
|
if not isinstance(p_details, dict): continue |
|
|
|
p_type_lower = str(p_type_original).lower() |
|
if "phạt tiền" in p_type_lower: |
|
current_chunk_metadata['has_fine'] = True |
|
if p_details.get("individual_fine_min") is not None: current_chunk_metadata['individual_fine_min'] = p_details.get("individual_fine_min") |
|
if p_details.get("individual_fine_max") is not None: current_chunk_metadata['individual_fine_max'] = p_details.get("individual_fine_max") |
|
if "trừ điểm" in p_type_lower or "điểm giấy phép lái xe" in p_type_lower : |
|
current_chunk_metadata['has_points_deduction'] = True |
|
if p_details.get("points_deducted") is not None: points_values.append(p_details.get("points_deducted")) |
|
if "tước quyền sử dụng giấy phép lái xe" in p_type_lower or "tước bằng lái" in p_type_lower: |
|
current_chunk_metadata['has_license_suspension'] = True |
|
if p_details.get("suspension_duration_min_months") is not None: s_min_months.append(float(p_details.get("suspension_duration_min_months"))) |
|
if p_details.get("suspension_duration_max_months") is not None: s_max_months.append(float(p_details.get("suspension_duration_max_months"))) |
|
if "tịch thu" in p_type_lower: |
|
current_chunk_metadata['has_confiscation'] = True |
|
if p_details.get("confiscation_item"): confiscation_items_list.append(str(p_details.get("confiscation_item"))) |
|
|
|
if penalty_types_for_this_point: current_chunk_metadata['penalty_types_str'] = ", ".join(sorted(list(set(penalty_types_for_this_point)))) |
|
if points_values: current_chunk_metadata['points_deducted_values_str'] = ", ".join(map(str, sorted(list(set(points_values))))) |
|
if s_min_months: current_chunk_metadata['suspension_min_months'] = min(s_min_months) |
|
if s_max_months: current_chunk_metadata['suspension_max_months'] = max(s_max_months) |
|
if confiscation_items_list: current_chunk_metadata['confiscation_items_str'] = ", ".join(sorted(list(set(confiscation_items_list)))) |
|
|
|
|
|
if point_data.get("speed_limit") is not None: current_chunk_metadata['speed_limit_value'] = point_data.get("speed_limit") |
|
if point_data.get("speed_limit_min") is not None: current_chunk_metadata['speed_limit_min_value'] = point_data.get("speed_limit_min") |
|
if point_data.get("speed_type"): current_chunk_metadata['speed_category'] = point_data.get("speed_type") |
|
|
|
|
|
speed_limits_extra = point_data.get("speed_limits_by_vehicle_type_and_road_type", []) |
|
point_specific_vehicle_types_raw: List[str] = [] |
|
point_specific_road_types: List[str] = [] |
|
if isinstance(speed_limits_extra, list): |
|
for sl_item in speed_limits_extra: |
|
if isinstance(sl_item, dict): |
|
if sl_item.get("vehicle_type"): point_specific_vehicle_types_raw.append(str(sl_item.get("vehicle_type")).lower()) |
|
if sl_item.get("road_type"): point_specific_road_types.append(str(sl_item.get("road_type")).lower()) |
|
if point_specific_vehicle_types_raw: current_chunk_metadata['point_specific_vehicle_types_str'] = ", ".join(sorted(list(set(point_specific_vehicle_types_raw)))) |
|
if point_specific_road_types: current_chunk_metadata['point_specific_road_types_str'] = ", ".join(sorted(list(set(point_specific_road_types)))) |
|
|
|
|
|
derived_vehicle_type_from_point = "không xác định" |
|
if point_specific_vehicle_types_raw: |
|
normalized_types_from_point_data = set() |
|
for vt_raw in set(point_specific_vehicle_types_raw): |
|
standard_type = get_standardized_vehicle_type(vt_raw) |
|
if standard_type: normalized_types_from_point_data.add(standard_type) |
|
|
|
if len(normalized_types_from_point_data) == 1: |
|
derived_vehicle_type_from_point = list(normalized_types_from_point_data)[0] |
|
elif len(normalized_types_from_point_data) > 1: |
|
|
|
if "ô tô" in normalized_types_from_point_data and "xe máy" in normalized_types_from_point_data: |
|
derived_vehicle_type_from_point = "ô tô và xe máy" |
|
|
|
elif "ô tô" in normalized_types_from_point_data: derived_vehicle_type_from_point = "ô tô" |
|
elif "xe máy" in normalized_types_from_point_data: derived_vehicle_type_from_point = "xe máy" |
|
else: |
|
derived_vehicle_type_from_point = "nhiều loại cụ thể" |
|
|
|
|
|
|
|
clear_types = ["ô tô", "xe máy", "xe cơ giới", "xe thô sơ", "người đi bộ", "ô tô và xe máy"] |
|
if derived_vehicle_type_from_point not in clear_types and derived_vehicle_type_from_point != "không xác định": |
|
|
|
current_chunk_metadata['applicable_vehicle_type'] = article_level_vehicle_type |
|
elif derived_vehicle_type_from_point == "không xác định": |
|
current_chunk_metadata['applicable_vehicle_type'] = article_level_vehicle_type |
|
else: |
|
current_chunk_metadata['applicable_vehicle_type'] = derived_vehicle_type_from_point |
|
|
|
|
|
if point_data.get("applies_to"): current_chunk_metadata['applies_to_context'] = point_data.get("applies_to") |
|
if point_data.get("location"): current_chunk_metadata['specific_location_info'] = point_data.get("location") |
|
|
|
final_metadata_cleaned = {k: v for k, v in current_chunk_metadata.items() if v is not None} |
|
flat_list.append({ "text": chunk_text, "metadata": final_metadata_cleaned }) |
|
|
|
else: |
|
chunk_text = clause_data.get("clause_text_original") |
|
if chunk_text: |
|
current_clause_level_metadata = clause_metadata_base.copy() |
|
|
|
current_clause_level_metadata['applicable_vehicle_type'] = article_level_vehicle_type |
|
|
|
if current_clause_level_metadata.get("overall_fine_note_for_clause_text"): |
|
current_clause_level_metadata['has_fine_clause_level'] = True |
|
|
|
final_metadata_cleaned = {k:v for k,v in current_clause_level_metadata.items() if v is not None} |
|
flat_list.append({ "text": chunk_text, "metadata": final_metadata_cleaned }) |
|
return flat_list |
|
|
|
|
|
def analyze_query(query_text: str) -> Dict[str, Any]: |
|
""" |
|
Phân tích query để xác định ý định của người dùng (ví dụ: hỏi về phạt tiền, điểm, loại xe...). |
|
""" |
|
query_lower = query_text.lower() |
|
analysis: Dict[str, Any] = { |
|
"mentions_fine": bool(re.search(r'tiền|phạt|bao nhiêu đồng|bao nhiêu tiền|mức phạt|xử phạt hành chính|nộp phạt', query_lower)), |
|
"mentions_points": bool(re.search(r'điểm|trừ điểm|mấy điểm|trừ bao nhiêu điểm|bằng lái|gplx|giấy phép lái xe', query_lower)), |
|
"mentions_suspension": bool(re.search(r'tước bằng|tước giấy phép lái xe|giam bằng|treo bằng|thu bằng lái|tước quyền sử dụng', query_lower)), |
|
"mentions_confiscation": bool(re.search(r'tịch thu|thu xe|thu phương tiện', query_lower)), |
|
"mentions_max_speed": bool(re.search(r'tốc độ tối đa|giới hạn tốc độ|chạy quá tốc độ|vượt tốc độ', query_lower)), |
|
"mentions_min_speed": bool(re.search(r'tốc độ tối thiểu|chạy chậm hơn', query_lower)), |
|
"mentions_safe_distance": bool(re.search(r'khoảng cách an toàn|cự ly an toàn|cự ly tối thiểu|giữ khoảng cách', query_lower)), |
|
"mentions_remedial_measures": bool(re.search(r'biện pháp khắc phục|khắc phục hậu quả', query_lower)), |
|
"vehicle_type_query": None, |
|
} |
|
|
|
|
|
|
|
queried_vehicle_standardized = get_standardized_vehicle_type(query_lower) |
|
if queried_vehicle_standardized: |
|
analysis["vehicle_type_query"] = queried_vehicle_standardized |
|
|
|
return analysis |
|
|
|
|
|
def search_relevant_laws( |
|
query_text: str, |
|
embedding_model, |
|
faiss_index, |
|
chunks_data: List[Dict[str, Any]], |
|
bm25_model, |
|
k: int = 5, |
|
initial_k_multiplier: int = 10, |
|
rrf_k_constant: int = 60, |
|
|
|
boost_fine: float = 0.15, |
|
boost_points: float = 0.15, |
|
boost_both_fine_points: float = 0.10, |
|
boost_vehicle_type: float = 0.20, |
|
boost_suspension: float = 0.18, |
|
boost_confiscation: float = 0.18, |
|
boost_max_speed: float = 0.15, |
|
boost_min_speed: float = 0.15, |
|
boost_safe_distance: float = 0.12, |
|
boost_remedial_measures: float = 0.10 |
|
) -> List[Dict[str, Any]]: |
|
""" |
|
Thực hiện tìm kiếm kết hợp (semantic + keyword) với RRF và metadata re-ranking. |
|
""" |
|
if k <= 0: |
|
print("Lỗi: k (số lượng kết quả) phải là số dương.") |
|
return [] |
|
if not chunks_data: |
|
print("Lỗi: chunks_data rỗng, không thể tìm kiếm.") |
|
return [] |
|
|
|
print(f"\n🔎 Đang tìm kiếm (Hybrid) cho truy vấn: '{query_text}'") |
|
|
|
|
|
query_analysis = analyze_query(query_text) |
|
|
|
|
|
num_vectors_in_index = faiss_index.ntotal |
|
if num_vectors_in_index == 0: |
|
print("Lỗi: FAISS index rỗng.") |
|
return [] |
|
|
|
|
|
num_candidates_each_retriever = max(min(k * initial_k_multiplier, num_vectors_in_index), min(k, num_vectors_in_index)) |
|
if num_candidates_each_retriever == 0: |
|
print(f" Không thể lấy đủ số lượng ứng viên ban đầu (num_candidates = 0).") |
|
return [] |
|
|
|
|
|
semantic_indices_raw: np.ndarray = np.array([[]], dtype=int) |
|
try: |
|
query_embedding_tensor = embedding_model.encode( |
|
[query_text], convert_to_tensor=True, device=embedding_model.device |
|
) |
|
query_embedding_np = query_embedding_tensor.cpu().numpy().astype('float32') |
|
faiss.normalize_L2(query_embedding_np) |
|
|
|
|
|
_, semantic_indices_raw = faiss_index.search(query_embedding_np, num_candidates_each_retriever) |
|
|
|
except Exception as e: |
|
print(f"Lỗi khi tìm kiếm ngữ nghĩa (FAISS): {e}") |
|
|
|
|
|
|
|
|
|
tokenized_query_bm25 = tokenize_vi_for_bm25_setup(query_text) |
|
top_bm25_results: List[Dict[str, Any]] = [] |
|
try: |
|
if bm25_model and tokenized_query_bm25: |
|
all_bm25_scores = bm25_model.get_scores(tokenized_query_bm25) |
|
|
|
bm25_results_with_indices = [ |
|
{'index': i, 'score': score} for i, score in enumerate(all_bm25_scores) if score > 0 |
|
] |
|
|
|
bm25_results_with_indices.sort(key=lambda x: x['score'], reverse=True) |
|
top_bm25_results = bm25_results_with_indices[:num_candidates_each_retriever] |
|
|
|
else: |
|
|
|
pass |
|
except Exception as e: |
|
print(f"Lỗi khi tìm kiếm từ khóa (BM25): {e}") |
|
|
|
|
|
|
|
rrf_scores: Dict[int, float] = defaultdict(float) |
|
all_retrieved_indices_set: set[int] = set() |
|
|
|
if semantic_indices_raw.size > 0: |
|
for rank, doc_idx_int in enumerate(semantic_indices_raw[0]): |
|
doc_idx = int(doc_idx_int) |
|
if 0 <= doc_idx < len(chunks_data): |
|
rrf_scores[doc_idx] += 1.0 / (rrf_k_constant + rank) |
|
all_retrieved_indices_set.add(doc_idx) |
|
|
|
for rank, item in enumerate(top_bm25_results): |
|
doc_idx = item['index'] |
|
if 0 <= doc_idx < len(chunks_data): |
|
rrf_scores[doc_idx] += 1.0 / (rrf_k_constant + rank) |
|
all_retrieved_indices_set.add(doc_idx) |
|
|
|
fused_initial_results: List[Dict[str, Any]] = [] |
|
for doc_idx in all_retrieved_indices_set: |
|
fused_initial_results.append({ |
|
'index': doc_idx, |
|
'fused_score': rrf_scores[doc_idx] |
|
}) |
|
fused_initial_results.sort(key=lambda x: x['fused_score'], reverse=True) |
|
|
|
|
|
|
|
final_processed_results: List[Dict[str, Any]] = [] |
|
|
|
num_to_process_metadata = min(len(fused_initial_results), num_candidates_each_retriever * 2 if num_candidates_each_retriever > 0 else k * 3) |
|
|
|
|
|
for rank_idx, res_item in enumerate(fused_initial_results[:num_to_process_metadata]): |
|
result_index = res_item['index'] |
|
base_score_from_fusion = res_item['fused_score'] |
|
metadata_boost_components: Dict[str, float] = defaultdict(float) |
|
passes_all_strict_filters = True |
|
|
|
try: |
|
original_chunk = chunks_data[result_index] |
|
chunk_metadata = original_chunk.get('metadata', {}) |
|
chunk_text_lower = original_chunk.get('text', '').lower() |
|
|
|
|
|
has_fine_info_in_chunk = chunk_metadata.get("has_fine", False) or chunk_metadata.get("has_fine_clause_level", False) |
|
has_points_info_in_chunk = chunk_metadata.get("has_points_deduction", False) |
|
|
|
if query_analysis["mentions_fine"]: |
|
if has_fine_info_in_chunk: |
|
metadata_boost_components["fine"] += boost_fine |
|
elif not query_analysis["mentions_points"]: |
|
passes_all_strict_filters = False |
|
|
|
if query_analysis["mentions_points"]: |
|
if has_points_info_in_chunk: |
|
metadata_boost_components["points"] += boost_points |
|
elif not query_analysis["mentions_fine"]: |
|
passes_all_strict_filters = False |
|
|
|
if query_analysis["mentions_fine"] and query_analysis["mentions_points"]: |
|
if not has_fine_info_in_chunk and not has_points_info_in_chunk: |
|
passes_all_strict_filters = False |
|
elif has_fine_info_in_chunk and has_points_info_in_chunk: |
|
metadata_boost_components["both_fine_points"] += boost_both_fine_points |
|
|
|
|
|
queried_vehicle = query_analysis["vehicle_type_query"] |
|
if queried_vehicle: |
|
applicable_vehicle_meta = chunk_metadata.get("applicable_vehicle_type", "").lower() |
|
point_specific_vehicles_meta = chunk_metadata.get("point_specific_vehicle_types_str", "").lower() |
|
article_title_lower = chunk_metadata.get("article_title", "").lower() |
|
|
|
match_vehicle = False |
|
if queried_vehicle in applicable_vehicle_meta: match_vehicle = True |
|
elif queried_vehicle in point_specific_vehicles_meta: match_vehicle = True |
|
|
|
elif queried_vehicle in article_title_lower: match_vehicle = True |
|
|
|
elif queried_vehicle in chunk_text_lower: match_vehicle = True |
|
|
|
|
|
if match_vehicle: |
|
metadata_boost_components["vehicle_type"] += boost_vehicle_type |
|
else: |
|
|
|
|
|
if applicable_vehicle_meta and \ |
|
applicable_vehicle_meta not in ["không xác định", "nhiều loại cụ thể", "ô tô và xe máy"] and \ |
|
not (applicable_vehicle_meta == "xe cơ giới" and queried_vehicle in ["ô tô", "xe máy"]): |
|
passes_all_strict_filters = False |
|
|
|
|
|
if query_analysis["mentions_suspension"] and chunk_metadata.get("has_license_suspension"): |
|
metadata_boost_components["suspension"] += boost_suspension |
|
|
|
|
|
if query_analysis["mentions_confiscation"] and chunk_metadata.get("has_confiscation"): |
|
metadata_boost_components["confiscation"] += boost_confiscation |
|
|
|
|
|
if query_analysis["mentions_max_speed"]: |
|
if chunk_metadata.get("speed_limit_value") is not None or \ |
|
"tốc độ tối đa" in chunk_metadata.get("speed_category","").lower() or \ |
|
any(kw in chunk_text_lower for kw in ["quá tốc độ", "tốc độ tối đa", "vượt tốc độ quy định"]): |
|
metadata_boost_components["max_speed"] += boost_max_speed |
|
|
|
|
|
if query_analysis["mentions_min_speed"]: |
|
if chunk_metadata.get("speed_limit_min_value") is not None or \ |
|
"tốc độ tối thiểu" in chunk_metadata.get("speed_category","").lower() or \ |
|
"tốc độ tối thiểu" in chunk_text_lower: |
|
metadata_boost_components["min_speed"] += boost_min_speed |
|
|
|
|
|
if query_analysis["mentions_safe_distance"]: |
|
if any(kw in chunk_text_lower for kw in ["khoảng cách an toàn", "cự ly an toàn", "cự ly tối thiểu", "giữ khoảng cách"]): |
|
metadata_boost_components["safe_distance"] += boost_safe_distance |
|
|
|
|
|
if query_analysis["mentions_remedial_measures"]: |
|
if any(kw in chunk_text_lower for kw in ["biện pháp khắc phục", "khắc phục hậu quả"]): |
|
metadata_boost_components["remedial_measures"] += boost_remedial_measures |
|
|
|
if not passes_all_strict_filters: |
|
continue |
|
|
|
total_metadata_boost = sum(metadata_boost_components.values()) |
|
final_score_calculated = base_score_from_fusion + total_metadata_boost |
|
|
|
final_processed_results.append({ |
|
"rank_after_fusion": rank_idx + 1, |
|
"index": int(result_index), |
|
"base_score_rrf": float(base_score_from_fusion), |
|
"metadata_boost_components": dict(metadata_boost_components), |
|
"metadata_boost_total": float(total_metadata_boost), |
|
"final_score": final_score_calculated, |
|
"text": original_chunk.get('text', '*Không có text*'), |
|
"metadata": chunk_metadata, |
|
"query_analysis_for_boost": query_analysis |
|
}) |
|
|
|
except IndexError: |
|
print(f"Lỗi Index: Chỉ số {result_index} nằm ngoài chunks_data (size: {len(chunks_data)}). Bỏ qua chunk này.") |
|
except Exception as e: |
|
print(f"Lỗi khi xử lý ứng viên lai ghép tại chỉ số {result_index}: {e}. Bỏ qua chunk này.") |
|
|
|
final_processed_results.sort(key=lambda x: x["final_score"], reverse=True) |
|
final_results_top_k = final_processed_results[:k] |
|
|
|
print(f" ✅ Xử lý, kết hợp metadata boost và tái xếp hạng hoàn tất. Trả về {len(final_results_top_k)} kết quả.") |
|
return final_results_top_k |
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
print("Chạy test cho retrieval.py...") |
|
|
|
|
|
print("\n--- Test get_standardized_vehicle_type ---") |
|
test_vehicles = [ |
|
"người điều khiển xe ô tô con", "xe gắn máy", "xe cơ giới", "xe máy chuyên dùng", |
|
"xe đạp điện", "người đi bộ", "ô tô tải và xe mô tô", None, "" |
|
] |
|
for tv in test_vehicles: |
|
print(f"'{tv}' -> '{get_standardized_vehicle_type(tv)}'") |
|
|
|
|
|
print("\n--- Test analyze_query ---") |
|
test_queries = [ |
|
"xe máy không gương phạt bao nhiêu tiền?", |
|
"ô tô chạy quá tốc độ 20km bị trừ mấy điểm gplx", |
|
"đi bộ ở đâu thì đúng luật", |
|
"biện pháp khắc phục khi gây tai nạn là gì" |
|
] |
|
for tq in test_queries: |
|
print(f"Query: '{tq}'\nAnalysis: {json.dumps(analyze_query(tq), indent=2, ensure_ascii=False)}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print("\n--- Các test khác (ví dụ: search_relevant_laws) cần mock hoặc dữ liệu đầy đủ. ---") |