""" GIẢI THÍCH: Logic "Gõ không dấu" trong Trie ============================================ Câu hỏi: Làm thế nào để user gõ "lam sach" vẫn tìm ra "làm sạch"? """ import sys sys.path.append('src') import unicodedata from search.trie import Trie def explain_normalize(): """Giải thích chi tiết hàm _normalize()""" print("=" * 80) print("LOGIC: GỠ KHÔNG DẤU (NORMALIZE)") print("=" * 80) print(""" CÂU HỎI: -------- User gõ: "lam sach" (không dấu) Document: "làm sạch" (có dấu) → Làm sao tìm được? GIẢI PHÁP: NORMALIZE CẢ 2 BÊN ------------------------------ 1. Khi INSERT: Bỏ dấu document trước khi lưu vào Trie 2. Khi SEARCH: Bỏ dấu query trước khi tìm → Cả 2 đều thành "lam sach" → MATCH! HÀM _normalize() LÀM GÌ? ------------------------- Input: "Làm sạch WC" Output: "lam sach wc" Steps: 1. NFD Normalization: Tách dấu ra khỏi chữ "à" → "a" + "̀" (dấu huyền riêng) 2. Loại bỏ dấu: Xóa các ký tự dấu (category = 'Mn') "a" + "̀" → "a" 3. Lowercase: Chuyển về chữ thường "LAM" → "lam" """) input("\nNhấn Enter để xem demo code...\n") def demo_normalize_step_by_step(): """Demo từng bước của normalize""" print("\n" + "=" * 80) print("DEMO: TỪNG BƯỚC NORMALIZE") print("=" * 80) text = "Làm sạch WC" print(f"\n📝 Text gốc: '{text}'") print(f" Unicode: {[f'{c}(U+{ord(c):04X})' for c in text]}") # Bước 1: NFD Normalization print("\n" + "-" * 80) print("BƯỚC 1: NFD NORMALIZATION (Tách dấu)") print("-" * 80) nfd_text = unicodedata.normalize('NFD', text) print(f"Kết quả: '{nfd_text}'") print(f"Unicode: {[f'{c}(U+{ord(c):04X})' for c in nfd_text]}") print("\n💡 Giải thích:") print(" - 'à' (U+00E0) → 'a' (U+0061) + '̀' (U+0300)") print(" - 'ạ' (U+1EA1) → 'a' (U+0061) + '̣' (U+0323)") print(" - Dấu giờ là ký tự RIÊNG, dễ loại bỏ!") # Bước 2: Loại bỏ dấu print("\n" + "-" * 80) print("BƯỚC 2: LOẠI BỎ DẤU (Remove Marks)") print("-" * 80) no_marks = ''.join( char for char in nfd_text if unicodedata.category(char) != 'Mn' # Mn = Mark, Nonspacing ) print(f"Kết quả: '{no_marks}'") print(f"Unicode: {[f'{c}(U+{ord(c):04X})' for c in no_marks]}") print("\n💡 Giải thích:") print(" - Loại bỏ tất cả ký tự có category = 'Mn'") print(" - 'Mn' = Mark, Nonspacing (dấu thanh, dấu sắc, huyền...)") print(" - Chỉ giữ lại chữ cái thuần") # Bước 3: Lowercase print("\n" + "-" * 80) print("BƯỚC 3: LOWERCASE") print("-" * 80) final = no_marks.lower() print(f"Kết quả: '{final}'") print("\n💡 Giải thích:") print(" - Chuyển về chữ thường để không phân biệt hoa/thường") print("\n" + "=" * 80) print(f"✅ KẾT QUẢ CUỐI: '{text}' → '{final}'") print("=" * 80) input("\nNhấn Enter tiếp...\n") def demo_unicode_categories(): """Demo các Unicode categories""" print("\n" + "=" * 80) print("UNICODE CATEGORIES (Quan trọng!)") print("=" * 80) print(""" UNICODE CATEGORY LÀ GÌ? ------------------------ Mỗi ký tự Unicode có 1 category (loại): - Lu: Letter, Uppercase (A, B, C) - Ll: Letter, Lowercase (a, b, c) - Mn: Mark, Nonspacing (dấu sắc, huyền, hỏi...) - Zs: Separator, Space (khoảng trắng) - Po: Punctuation, Other (., !, ?) - ... → Ta chỉ cần loại bỏ 'Mn' để bỏ dấu! """) # Demo các ký tự test_chars = [ ('a', 'Chữ a thường'), ('à', 'Chữ à có dấu huyền (1 ký tự)'), ('̀', 'Dấu huyền riêng (sau NFD)'), ('ạ', 'Chữ a dấu nặng'), ('̣', 'Dấu nặng riêng (sau NFD)'), (' ', 'Khoảng trắng'), ('!', 'Dấu chấm than'), ] print("\n📋 TABLE: Ký tự và Category") print("-" * 80) print(f"{'Ký tự':<10} {'Unicode':<15} {'Category':<10} {'Mô tả'}") print("-" * 80) for char, desc in test_chars: if char in ['̀', '̣']: # Dấu cần NFD trước display = f"'{char}'" else: # NFD để xem dấu nfd_char = unicodedata.normalize('NFD', char) if len(nfd_char) > 1: # Có dấu riêng base = nfd_char[0] mark = nfd_char[1] category_base = unicodedata.category(base) category_mark = unicodedata.category(mark) print(f"'{char}' U+{ord(char):04X} {category_base} {desc}") print(f" └─ '{mark}' U+{ord(mark):04X} {category_mark} (dấu riêng)") continue code = ord(char) category = unicodedata.category(char) print(f"'{char}' U+{code:04X} {category} {desc}") print("\n💡 QUAN TRỌNG:") print(" - Category 'Mn' = Mark, Nonspacing = DẤU") print(" - Loại bỏ 'Mn' = Bỏ dấu!") input("\nNhấn Enter tiếp...\n") def demo_insert_and_search_flow(): """Demo flow INSERT và SEARCH với normalize""" print("\n" + "=" * 80) print("FLOW: INSERT VÀ SEARCH VỚI NORMALIZE") print("=" * 80) trie = Trie(max_results=10) # INSERT print("\n📥 INSERT: Thêm 'Làm sạch WC'") print("-" * 80) original = "Làm sạch WC" print(f"1. Text gốc: '{original}'") # Normalize normalized = unicodedata.normalize('NFD', original) normalized = ''.join(c for c in normalized if unicodedata.category(c) != 'Mn') normalized = normalized.lower() print(f"2. Sau normalize: '{normalized}'") print(f"3. Lưu vào Trie: '{normalized}'") # Thực tế insert trie.insert(original, doc_id=0, score=1.0, index_word_starts=True, normalize=True) print(f"4. ✓ Đã insert vào Trie") # SEARCH print("\n\n🔍 SEARCH: User gõ 'lam' (không dấu)") print("-" * 80) query = "lam" print(f"1. Query gốc: '{query}'") print(f"2. Sau normalize: '{query}' (đã không dấu từ đầu)") print(f"3. Tìm trong Trie với prefix: '{query}'") results = trie.search_prefix(query, normalize=True) if results: print(f"4. ✓ Tìm thấy {len(results)} kết quả:") for doc_id, score in results: print(f" [{doc_id}] (score: {score})") print("\n💡 TẠI SAO TÌM ĐƯỢC?") print("-" * 80) print(f" - Document trong Trie: 'lam sach wc'") print(f" - Query search: 'lam'") print(f" - 'lam sach wc'.startswith('lam') = True") print(f" → MATCH!") input("\nNhấn Enter để xem code...\n") def show_code(): """Hiển thị code thực tế""" print("\n" + "=" * 80) print("CODE THỰC TẾ TRONG trie.py") print("=" * 80) print(""" def _normalize(self, text: str) -> str: \"\"\" Normalize text: Bỏ dấu tiếng Việt Example: >>> self._normalize("Làm sạch WC") "lam sach wc" \"\"\" # BƯỚC 1: NFD - Tách dấu ra khỏi chữ text = unicodedata.normalize('NFD', text) # BƯỚC 2: Loại bỏ dấu (category = 'Mn') text = ''.join( char for char in text if unicodedata.category(char) != 'Mn' # ← KEY LINE! ) # BƯỚC 3: Lowercase return text.lower() def insert(self, text, doc_id, normalize=True, ...): if normalize: text = self._normalize(text) # ← Normalize trước khi insert # ... insert vào Trie def search_prefix(self, prefix, normalize=True): if normalize: prefix = self._normalize(prefix) # ← Normalize trước khi search # ... search trong Trie """) print("\n💡 KEY POINTS:") print("-" * 80) print("1. Normalize CẢ INSERT và SEARCH") print("2. Dùng NFD để tách dấu") print("3. Loại bỏ category 'Mn' (Mark, Nonspacing)") print("4. Lowercase để không phân biệt hoa/thường") input("\nNhấn Enter để xem kết luận...\n") def conclusion(): """Kết luận""" print("\n" + "=" * 80) print("📝 KẾT LUẬN") print("=" * 80) print(""" ✅ LOGIC "GỠ KHÔNG DẤU": ------------------------- 1. INSERT: Normalize text trước khi lưu "Làm sạch" → "lam sach" 2. SEARCH: Normalize query trước khi tìm "lam" → "lam" (đã không dấu) 3. MATCH: Prefix matching trên text đã normalize "lam sach".startswith("lam") = True 🔑 KEY: unicodedata.category(char) != 'Mn' ------------------------------------------ - 'Mn' = Mark, Nonspacing = DẤU thanh - Loại bỏ 'Mn' = Bỏ dấu tiếng Việt - NFD trước để tách dấu thành ký tự riêng 📊 COMPLEXITY: -------------- - Normalize: O(n) với n = độ dài text - Không ảnh hưởng đến performance của Trie - Chỉ chạy 1 lần khi insert/search 🎯 LỢI ÍCH: ------------ ✓ User gõ linh hoạt: có dấu hoặc không dấu ✓ Không cần 2 index riêng ✓ Memory hiệu quả ✓ UX tốt hơn (user không cần gõ dấu) ⚠️ TRADE-OFF: -------------- - Mất thông tin dấu gốc - Cần giữ document gốc riêng (có dấu) - "lam" sẽ match cả "làm", "lạm", "lãm"... (nhưng OK cho search autocomplete) """) print("=" * 80) print("\n✅ Hiểu rồi chứ? Logic đơn giản nhưng hiệu quả!") print("📂 Code: src/search/trie.py → _normalize()") print("🔑 Key: unicodedata.category(char) != 'Mn'\n") if __name__ == "__main__": explain_normalize() demo_normalize_step_by_step() demo_unicode_categories() demo_insert_and_search_flow() show_code() conclusion()