Là một SRE (Site Reliability Engineer), mình thường xuyên phải bơi trong biển log, hàng núi dữ liệu và vô vàn file. Mà không chỉ SRE đâu, bạn nào cũng thế thôi, dù là lướt web hay dùng máy tính cá nhân, chúng ta đều đối mặt với một vấn đề nhức nhối: Làm sao tìm được thứ mình cần trong cái kho tài liệu ngày càng phình to? Đặt ra một bài toán thế này nhé: Nếu mình gõ từ "Panda" vào ô tìm kiếm trên máy tính, file nào sẽ được ưu tiên hiển thị trước tiên? File mới nhất à? Không đâu, phải là file *liên quan nhất* chứ! Và làm thế nào để đánh giá mức độ "liên quan" của một tài liệu so với truy vấn tìm kiếm chính là chủ đề nóng hổi của chúng ta ngày hôm nay! Nào, chuẩn bị trà, cà phê hay socola nóng đi nhé, chúng ta cùng "nhảy dù" vào thế giới tìm kiếm thôi! Vấn đề của chúng ta hôm nay có thể tóm gọn lại là: Với mỗi truy vấn tìm kiếm, chúng ta muốn gán một 'điểm số liên quan' cho từng tài liệu. Tài liệu nào có điểm cao nhất sẽ nghiễm nhiên đứng đầu danh sách kết quả! À mà khoan, có một lưu ý nhỏ xíu này: Bài viết này có 'dính dáng' đến vài công thức toán học đấy. Nhưng đừng hoảng hốt nha! Chúng ta sẽ cùng nhau 'giải mã' từng bước một, mình sẽ giải thích thật dễ hiểu để bạn không bị 'rối não' đâu. Hứa đó! Để dễ hình dung, hãy cùng mình làm quen với 'đội hình' tài liệu thử nghiệm của chúng ta nhé: * **Doc 1:** "Un panda est un animal blanc et noir" (Một con gấu trúc là động vật trắng và đen) * **Doc 2:** "Le chien est blanc" (Con chó màu trắng) * **Doc 3:** "Le chat est noir" (Con mèo màu đen) * **Doc 4:** "Le panda n'est ni un chat ni un chien" (Con gấu trúc không phải mèo cũng không phải chó) * **Doc 5:** "Le panda roux est roux" (Gấu trúc đỏ có màu đỏ) * **Doc 6:** "Noir c'est noir, il n'y a vraiment plus d'espoir Je suis dans le noir, j'ai du mal à croire Noir c'est noir, il n'est jamais trop tard Noir c'est noir, il me reste l'espoir Noir c'est noir, il me reste l'espoir Noir c'est noir, il me reste l'espoir" (Đen thì là đen, không còn hy vọng nào nữa. Tôi ở trong bóng tối, khó mà tin được. Đen thì là đen, không bao giờ là quá muộn. Đen thì là đen, tôi vẫn còn hy vọng. Đen thì là đen, tôi vẫn còn hy vọng. Đen thì là đen, tôi vẫn còn hy vọng) Bộ tài liệu này tuy đơn giản nhưng cực kỳ hữu ích để chúng ta thấy được cái 'khó nhằn' của việc chấm điểm liên quan đó! **Token: 'Mảnh ghép' của dữ liệu** Đầu tiên là 'Token' – Nghe có vẻ 'hàn lâm' nhưng đơn giản lắm! Các tài liệu và cả truy vấn tìm kiếm của chúng ta không phải được tạo thành từ 'từ' mà là từ 'token'. Trong bài viết này, bạn cứ coi mỗi 'từ' là một 'token' nhé. Thế nên cũng chẳng có gì thay đổi lớn đâu, nhưng vì mình sẽ dùng từ 'token' xuyên suốt bài viết nên cứ gọi là làm quen trước cho tiện. Ví dụ, tài liệu số 1 của chúng ta sẽ bao gồm các token sau: ['Un', 'panda', 'est', 'un', 'animal', 'blanc', 'et', 'noir']. **Chỉ mục Đảo ngược (Inverted Index): 'Cuốn sổ vàng' của việc tìm kiếm** Tiếp theo là 'Chỉ mục Đảo ngược' (Inverted Index) – Hay còn gọi là 'Cuốn sổ vàng' của việc tìm kiếm! Khi bạn muốn tìm từ 'noir' (đen) trong mớ tài liệu của mình, chúng ta đâu thể ngồi mở từng file một để xem từ đó có trong đó không, đúng không? Trước khi bất kỳ thao tác tìm kiếm nào diễn ra, ứng dụng của chúng ta phải 'lập chỉ mục' (index) các tài liệu. Tức là, nó sẽ 'quét' qua từng tài liệu để rút trích ra các token có trong đó. Trong quá trình này, chúng ta sẽ xây dựng một thứ gọi là 'chỉ mục đảo ngược'. Bạn hình dung đơn giản thế này: Chỉ mục đảo ngược là một 'từ điển đặc biệt' nơi mà: Từ khóa (key) chính là một token (ví dụ: 'panda', 'noir'). Giá trị (value) đi kèm với từ khóa đó là danh sách các tài liệu có chứa token đó. Ví dụ nè: Token 'panda' sẽ dẫn bạn đến các tài liệu 1, 4 và 5. Token 'noir' sẽ chỉ cho bạn thấy các tài liệu 1, 3 và 6. Cứ thế mà triển khai! Để tiện hình dung hơn về cấu trúc dữ liệu, chúng ta sẽ có hai 'nhân vật chính': Một là lớp `Document` (Tài liệu), đại diện cho mỗi tài liệu của chúng ta, chứa các thông tin như ID, tên, đường dẫn, nội dung, và cả các token đã được phân tích. Hai là lớp `SearchResult` (Kết quả tìm kiếm), là cái mà chúng ta sẽ trả về cho người dùng sau khi tìm kiếm xong. Nó sẽ bao gồm ID, tên, đường dẫn, đoạn trích nội dung và quan trọng nhất là 'điểm số liên quan' (Score) mà chúng ta sẽ tính toán! **Cách chấm điểm 'cây nhà lá vườn', đơn giản nhất!** <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/simple_score.png' alt='Phép tính điểm đơn giản'> Ban đầu, chúng ta có thể nghĩ đến một cách chấm điểm cực kỳ 'thẳng thắn': Đếm xem một token (từ khóa) xuất hiện bao nhiêu lần trong tài liệu là cho bấy nhiêu điểm! Ví dụ, mình tìm từ 'noir' (đen). Nó xuất hiện 1 lần trong tài liệu 1, 1 lần trong tài liệu 3 và... 11 lần trong tài liệu 6. Vậy thì, tài liệu 1 và 3 được 1 điểm, còn tài liệu 6 'ăn đứt' với 11 điểm! Nghe có vẻ hợp lý đúng không? Nhưng mà, 'đời không như là mơ' các bạn ạ! <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/count_score_problem.png' alt='Vấn đề của điểm đếm đơn giản'> Vấn đề ở đây là gì? Số lần xuất hiện không phải lúc nào cũng là thước đo tốt nhất. Thử tưởng tượng, nếu truy vấn của mình là 'chat' (mèo) và 'noir' (đen). Tài liệu 6, dù chỉ chứa mỗi từ 'noir' 11 lần, vẫn 'chễm chệ' 11 điểm. Trong khi đó, tài liệu 3 ('Le chat est noir'), rõ ràng chứa cả hai từ khóa, lại chỉ được vỏn vẹn 2 điểm. Thật vô lý phải không? Tài liệu 3 mới là cái chúng ta cần tìm chứ! Để khắc phục, chúng ta có thể thử cộng thêm điểm khi một tài liệu chứa tất cả các token trong truy vấn. Nhưng quan trọng hơn, chúng ta nên tìm cách giảm bớt 'sức nặng' của một token khi nó xuất hiện quá nhiều lần trong một tài liệu. Liệu tài liệu nào có 50 từ 'noir' thì nên có điểm cao gấp đôi so với tài liệu có 25 từ 'noir' không? Chắc là không rồi! Hơn một chút thì có lý, nhưng gấp đôi thì 'hơi quá đà' đó! Và nữa, tài liệu 1 và 3 đều có 1 điểm cho từ 'noir', chúng ta chẳng có cách nào để biết cái nào liên quan hơn cả. **'Nâng cấp' cách chấm điểm** <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/improved_score_code.png' alt='Cải thiện hàm tính điểm'> Để giải quyết những khúc mắc trên, chúng ta có thể 'tút tát' lại hàm tính điểm của mình một chút: 1. Tính điểm cho tất cả các token trong truy vấn. 2. Hạn chế 'sức ảnh hưởng' của một token đơn lẻ lên tổng điểm. 3. 'Thưởng nóng' cho những tài liệu khớp với nhiều token. Cụ thể, mình sẽ cài đặt là: một token không thể đóng góp quá 5 điểm vào tổng điểm. Và tài liệu sẽ được cộng thêm 10 điểm cho mỗi token khớp trong truy vấn. Giờ hãy thử lại với truy vấn 'chat' (mèo), 'noir' (đen) nhé: * Tài liệu 1 và 4 (chỉ có một token khớp): Mỗi cái 1 điểm cho tần suất + 10 điểm cho việc khớp một token = 11 điểm. * Tài liệu 3 ('Le chat est noir' - khớp cả 2 token): 2 điểm cho tần suất + 20 điểm cho việc khớp hai token = 22 điểm. * Tài liệu 6 (chứa 11 từ 'noir' nhưng bị giới hạn tần suất ở 5): 5 điểm cho tần suất + 10 điểm cho việc khớp một token = 15 điểm. <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/improved_score_results.png' alt='Kết quả sau khi cải thiện hàm tính điểm'> Tuyệt vời! Chúng ta đã giải quyết được hai vấn đề ban đầu. Tài liệu 3 giờ đây đã 'vượt mặt' các tài liệu khác một cách xứng đáng. Nhưng liệu có thể 'ngon' hơn nữa không? Vấn đề 'đau đầu' ở đây chính là cái giới hạn '5 điểm' đó. Không phải con số 5 là vấn đề, mà là việc chúng ta đang có một sự tăng điểm tuyến tính rồi 'khựng lại' đột ngột ở một giới hạn tùy ý. Điều này chưa thật sự tự nhiên và linh hoạt. **Quy luật 'Hiệu suất giảm dần' (Diminishing Returns)** <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0obt80iu7vvwr8mssgp.png' alt='Công thức hiệu suất giảm dần'> Để giải quyết triệt để vấn đề trên, thay vì đặt một giới hạn cứng nhắc, chúng ta sẽ áp dụng 'quy luật hiệu suất giảm dần'. Nghe có vẻ phức tạp nhưng hiểu đơn giản là: lần xuất hiện đầu tiên của token sẽ mang lại 1 điểm, lần thứ hai 0.95 điểm, lần thứ ba 0.92 điểm... Tức là càng xuất hiện nhiều, mỗi lần xuất hiện sau sẽ đóng góp ít hơn vào tổng điểm, chứ không bị 'cắt cụt' đột ngột. Chúng ta sẽ dùng công thức này với tham số `decay` (hệ số suy giảm) là 0.97. <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5l6w3aa5utxy7xv2m63.png' alt='Ví dụ tính toán hiệu suất giảm dần cho tần suất 6'> Với tần suất 6, điểm nhận được là 5.4. Trước đây là 6. Chưa thấy sự khác biệt rõ rệt đúng không? Vậy hãy xem khi tần suất tăng lên nhé: * Với 20 lần xuất hiện: <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Funh9fo05f8nuc21jxvfz.png' alt='Tính toán hiệu suất giảm dần cho tần suất 20'> (Đạt 11.5 điểm) * Với 50 lần xuất hiện: <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5vswkv1zv05xpdydh4x.png' alt='Tính toán hiệu suất giảm dần cho tần suất 50'> (Đạt 19.3 điểm) * Với 100 lần xuất hiện: <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvsmufxulw8hy6de3yeo6.png' alt='Tính toán hiệu suất giảm dần cho tần suất 100'> (Đạt 24.8 điểm) Thấy chưa? Chúng ta giờ đây có thể phân biệt được hai tài liệu có số lần xuất hiện token khác nhau, mà không hề 'ưu ái' quá mức một tài liệu chứa từ khóa quá nhiều lần. 'Sức ảnh hưởng' của từ khóa lên điểm số sẽ giảm dần một cách mượt mà, phi tuyến tính, thay vì bị 'cắt phéng' đột ngột. Đây là đoạn code cho hàm tính điểm mới của chúng ta: <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/diminishing_returns_code.png' alt='Code cho hiệu suất giảm dần'> Nhưng mà, liệu còn 'chiêu' nào nữa không? Hay nói cách khác, chúng ta còn gặp phải vấn đề gì? Hãy thử truy vấn: 'le' (cái/con/ông/bà – mạo từ xác định), 'animal' (động vật). Nếu chỉ dựa vào điểm tần suất (Term Frequency – TF) như vừa rồi (và chúng ta có thể làm thế, vì không có tài liệu nào khớp cả hai token, nên phần 'boost' sẽ không ảnh hưởng), các tài liệu chứa token 'le' sẽ đều có cùng điểm là 1: * doc2 – 1 * doc3 – 1 * doc5 – 1 * doc6 – 1 Đối với token 'animal', chỉ xuất hiện một lần trong tài liệu 1, cũng sẽ nhận được điểm là 1. Lại một lần nữa, chúng ta gặp phải vấn đề 'không phân biệt được' giữa các tài liệu. Bạn có thể nghĩ: 'À, mỗi từ chỉ xuất hiện một lần trong mỗi tài liệu, thì làm sao mà phân biệt được nữa?'. SAI LẦM! Có một điểm khác biệt cực kỳ quan trọng: đó là tần suất của chúng trong *tất cả* các tài liệu, chứ không phải chỉ trong một tài liệu riêng lẻ. Từ 'le' xuất hiện trong gần như tất cả các tài liệu, trong khi 'animal' chỉ xuất hiện trong duy nhất một tài liệu. Tức là có sự khác biệt về 'độ hiếm' – và chúng ta có thể 'khai thác' được sự hiếm có này! **TF-IDF: Sức mạnh của 'độ hiếm'** Đến giờ phút này, chúng ta mới chỉ tính toán cái gọi là TF (Term Frequency – Tần suất từ), tức là tần suất xuất hiện của một token trong một tài liệu cụ thể. Đã đến lúc bổ sung thêm một yếu tố 'siêu to khổng lồ' vào công thức tính điểm của chúng ta: đó là IDF (Inverse Document Frequency – Tần suất tài liệu nghịch đảo). IDF sẽ giúp chúng ta đo lường 'độ hiếm' của một từ trong *tập hợp tất cả các tài liệu* (corpus). Ý tưởng 'đỉnh cao' ở đây là: một từ càng hiếm, nó càng mang nhiều thông tin. Điều này hoàn toàn đúng trong ví dụ của chúng ta: token 'le' xuất hiện khắp nơi nên chẳng giúp gì mấy trong việc 'lọc' kết quả, trong khi 'animal' lại cực kỳ đặc trưng. Để tính IDF, chúng ta sẽ dùng công thức sau: <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jurwjgprwmw4qnmw1z8.png' alt='Công thức tính IDF'> Trong đó: * `t` là token cần tính. * `N` là tổng số tài liệu trong 'kho tàng' của chúng ta. * `nt` là số lượng tài liệu có chứa token `t`. Giờ hãy tính IDF cho 'le' nhé (có 6 tài liệu, 'le' xuất hiện trong 5 tài liệu): <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosx92876vtt5b1cxzq51.png' alt='Tính IDF cho từ le'> (0.079) Và cho 'animal' (có 6 tài liệu, 'animal' xuất hiện trong 1 tài liệu): <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapo71t9dvs885izzd98n.png' alt='Tính IDF cho từ animal'> (0.778) Cuối cùng, chúng ta sẽ 'kết duyên' TF và IDF lại với nhau để có điểm TF-IDF (đơn giản là nhân TF với IDF): <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/tfidf_score.png' alt='Kết quả điểm TF-IDF'> Vì TF của chúng ta ở ví dụ này đều là 1, nên điểm TF-IDF chính là giá trị IDF. Điểm số của các tài liệu cho truy vấn 'le', 'animal' giờ sẽ là: * doc1 – 0.778 ('animal' xuất hiện ở đây) * doc2 – 0.079 ('le' xuất hiện ở đây) * doc3 – 0.079 ('le' xuất hiện ở đây) * doc4 – 0.079 ('le' xuất hiện ở đây) * doc5 – 0.079 ('le' xuất hiện ở đây) * doc6 – 0.079 ('le' xuất hiện ở đây) Và nếu có một tài liệu chỉ chứa cả hai token 'le' và 'animal', nó sẽ đứng đầu với điểm 0.857. Bạn để ý không? Chúng ta thậm chí không cần phải 'tăng điểm nhân tạo' (boost) dựa trên số lượng token khớp trong truy vấn nữa – điều mà trước đây khá 'tùy tiện'. Tuyệt vời hơn rất nhiều phải không nào? Đây là đoạn code 'xịn xò' để tính TF-IDF: <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/tfidf_code.png' alt='Code tính TF-IDF'> Việc tính tần suất không thay đổi. Chúng ta chỉ cần lấy tần suất tài liệu chứa token (df) từ chỉ mục đảo ngược đã xây dựng. Tổng số tài liệu cũng đã biết. Thế là có IDF, có TF, và có điểm cuối cùng! Nhưng liệu có thể 'cải lùi' (cải thiện) hơn nữa không? Hãy xem những giới hạn hiện tại (dù đã rất ổn rồi). Một trong những vấn đề là TF: như chúng ta thấy, nó có thể tăng rất nhanh và làm 'lệch' điểm số. Ban đầu, chúng ta đã đặt ra một giới hạn để tránh điều này. Giới hạn này, về cơ bản, không có trong TF-IDF 'chuẩn mực'. Nhưng dù vậy, thuật toán này vẫn có xu hướng 'ưu ái' những tài liệu dài (thông qua TF), trong khi một tài liệu dài chưa chắc đã liên quan hơn. Hơn nữa, chúng ta có rất ít 'tham số' để tinh chỉnh kết quả (ngoại trừ tham số `decay` mà bản thân nó cũng không có trong phiên bản gốc). Vậy nên, đã đến lúc 'chào sân' một thuật toán mới: BM25! **Best Match 25 (BM25): 'Trùm cuối' của độ liên quan** <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7iqekvpq5qfos8k32yif.png' alt='Công thức BM25'> Best Match 25 là phiên bản thứ 25 của thuật toán được tạo ra để 'vá' những lỗ hổng của TF-IDF. Nghe công thức có vẻ 'kinh dị' đúng không? Đừng sợ! Nếu bạn đã 'sống sót' đến tận đây, bạn hoàn toàn có thể hiểu được nó. Chúng ta sẽ cùng nhau 'mổ xẻ' từng phần. IDF thì chúng ta đã quá quen thuộc rồi, không vấn đề gì. Giờ hãy xem cái gì sẽ thay thế cho TF nhé. Bắt đầu với tử số: <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1ep4dtvfdosbd1jjtf4.png' alt='Tử số của BM25'> * `f(t,d)`: Tần suất của từ `t` trong tài liệu `d`. * `k1 + 1`: `k1` là một tham số để kiểm soát 'độ bão hòa', giống như tham số `decay` của chúng ta lúc nãy. `k1` càng nhỏ, độ bão hòa càng nhanh, từ đó sẽ giảm phi tuyến tính 'sức ảnh hưởng' của mỗi token mới xuất hiện trong tài liệu. Tiếp theo là mẫu số: <img src='https://truyentranh.letranglan.top/api/v1/proxy?url=https://i.imgur.com/bm25_denominator_components.png' alt='Mẫu số của BM25'> Ở đây, chúng ta vẫn có tần suất `f(t,d)` và `k1` để kiểm soát độ bão hòa. Ngoài ra, còn có một yếu tố 'chuẩn hóa' dựa trên độ dài của tài liệu: * `
Khám phá cách chúng tôi kết hợp Orama.search, OpenAI và Sanity để tạo ra trải nghiệm tìm kiếm cá nhân hóa, tối ưu hóa tỷ lệ chuyển đổi cho website bán áo padel. Học hỏi về tìm kiếm vector, tự động hóa dữ liệu và UX thông minh.
Khám phá cách kết hợp SQL và AI (watsonx.ai) để truy vấn Elasticsearch dễ dàng mà không cần Query DSL phức tạp, giúp sếp và đồng nghiệp không rành kỹ thuật cũng có thể tự hỏi đáp dữ liệu.