Xin chào độc giả của 2coffee.dev. Các bạn ở Hà Nội có thấy tuần vừa rồi không khí mùa thu đã trở nên rõ rệt hơn không? Buổi sáng trời mát lạnh còn chiều tối thì đi kèm với những cơn gió lớn. Nhưng đằng sau đó lại là một tuần bận rộn đối với tôi. Vừa tập trung vào chạy "deadline" cho dự án ở công ty, tối về tranh thủ hoàn thiện chức năng tìm kiếm cho blog. Cái deadline này khác hẳn với mọi khi vì là tính năng chủ lực của năm cho sản phẩm. Còn về blog, tính năng tìm kiếm sớm hay muộn gì cũng phải hoàn thành, và đây là thời điểm thích hợp để làm điều đó.
Trước khi chuyển sang Fresh, blog vốn đã có tính năng tìm kiếm. Cách làm lúc đó là sử dụng fulltext-search của postgres. Nói thêm cho bạn đọc chưa biết, trước khi dùng postgres thì tôi đã dùng cả redisearch để phục vụ tìm kiếm. Nhìn chung thì postgres vẫn cho ra kết quả tốt hơn trong khi redisearch thì phức tạp hơn. Mà thực tế thì dữ liệu bài viết không nhiều đến mức để redisearch phát huy được hết tác dụng.
Thời điểm chuyển sang Fresh, lúc đó AI đang bùng nổ. Rất nhiều ánh nhìn đổ sang phía AI và cả những gì mà nó làm được. Sau khi hoàn thành những chức năng cơ bản đến bước chuẩn bị làm chức năng tìm kiếm thì tôi chợt nghĩ: "Hay là mình cũng thử ứng dụng AI!?". Thế là tôi quyết định "release" phiên bản blog mới mà không có chức năng tìm kiếm.
Để làm được tính năng tìm kiếm có sự góp mặt của AI thì tôi phải dành thời gian nghiên cứu cũng như thử nghiệm rất nhiều. Tìm hiểu về cách thức thực hiện, cách sử dụng các mô hình LLMs, mô hình embeddings, kiểu dữ liệu vector, cách biến dữ liệu thành vector và cả cách truy vấn...
Nói một cách ngắn gọn, vector là một tập hợp hữu hạn của các con số như trong toán học, số lượng của các con số đó tạo thành kích thước (dimension) của vector. Kích thước càng lớn, vector càng có khả năng tổng quát hóa dữ liệu mà nó biểu thị. Để biến một dữ liệu thông thường (văn bản, giọng nói, hình ảnh...) sang vector thì có nhiều cách, nhưng nhờ sự phổ biến của các mô hình LLMs hiện nay mà chỉ cần dưa dữ liệu vào một mô hình embeddings nào đó thì sẽ cho ra dữ liệu vector.
Lại nói về tìm kiếm ngữ nghĩa (semantic) nó khác với tìm kiếm theo keyword (fulltext) truyền thống. Tìm kiếm fulltext sẽ dựa vào lượng ký tự văn bản nhập vào để so khớp và cho ra những đoạn có chứa nhiều hoặc khớp nhất với từ khoá. Trong khi tìm kiếm ngữ nghĩa lại theo khuynh hướng nội dung. Giả sử bài viết của bạn đang giải thích cách hoạt động của node.js thì khi tìm kiếm theo cụm từ "node.js hoạt động như thế nào?" khả năng cao semantic sẽ tìm thấy được bài viết này. Trong khi đó fulltext sẽ cố tìm xem bài viết nào có chứa các từ "node.js", "hoạt", "động", "như"... Tất nhiên là không đến mức đó vì các thuật toán fulltext đã có thể loại bỏ được những từ "thừa" hoặc gom nhóm những từ đồng nghĩa...
Để truy vấn dữ liệu vector cần tối thiểu 2 bước. Đầu tiên là biến câu truy vấn thành vector, sau đó sử dụng các hàm truy vấn. Ví dụ với pg-vector - là một extensions hỗ trợ vector cho Postgres có các hàm truy vấn:
Bạn đọc có thể nhìn thấy L2 distance, Cosine distance, L1 distance... là các dạng so sánh vector. Tuỳ vào trường hợp sử dụng mà lựa chọn kiểu truy vấn cho hợp lý. Ví dụ như trong bài toán tìm kiếm, tôi lựa chọn kiểu Cosine distance - tức là hình dạng của 2 vector càng giống nhau thì càng tốt.
Đầu tiên là lựa chọn cơ sở dữ liệu phù hợp. Tôi đang dùng Turso làm cơ sở dữ liệu chính. Tuy nhiên Turso hoạt động dựa trên SQLite, không tối ưu cho dữ liệu dạng vector. Mặc dù họ có giới thiệu một extension để hỗ trợ cho vecter nhưng hơi phức tạp.
pg-vector thì ngược lại, được nhiều người sử dụng. Đây là một extensions dành cho Postgres. Nhắc đến Postgres thì lại nghĩ ngay đến Supabase - cho dùng miễn phí. Supabase tích hợp sẵn pg-vector, kích hoạt chỉ bằng một nút bấm nên nó là một sự lựa chọn tuyệt vời.
Bước tiếp theo là chọn models. Để tiết kiệm thì ngay từ đầu tôi đã tìm những models miễn phí, hoặc các dịch vụ cung cấp models miễn phí. Không thể không nhắc đến groq với các API Completions. Tuy nhiên groq không có các mô hình embeddings nên lại phải tìm một cái khác.
nomic-embed-text là một embeddings tìm thấy trong thư viện của Ollama. Nó có khả năng vector hoá văn bản. Ngoài ra Nomic cũng cung cấp API embeddings miễn phí kèm giới hạn. Nhưng cũng phải nhắc lại rằng nomic không hẳn là một mô hình đa ngôn ngữ, nó hỗ trợ tiếng Việt một cách hạn chế. Nên rất có thể vector cho ra không được tối ưa hoá theo ngữ nghĩa tiếng Việt.
Sau khi đã chuẩn bị xong các bước trên thì đến lượt viết mã để thêm dữ liệu vector và logic tìm kiếm.
Đầu tiên, cần chuyển nội dung bài viết thành vector và lưu vào Supabase. Ở đây thay vì chuyển toàn bộ nội dung bài viết thì tôi qua một bước tóm tắt lại nội dung chính của bài viết rồi mới đưa vào nomic-embed-text. Điều này giúp loại bỏ những ý phụ hoạ, tập trung vào nội dung chính và giảm lượng token đầu vào cho mô hình xử lý.
Một lưu ý nữa là mặc dù các models trên có API miễn phí nhưng chúng luôn đi kèm với giới hạn. Quá trình xử lý dữ liệu lần đầu rất tốn kém vì tôi có hơn 400 bài viết cả tiếng Việt lẫn tiếng Anh. Nên cách tốt hơn là chạy mô hình Llama 3.2 3B và nomic-embed-text ở dưới local. Ở đây sử dụng LM Studio.
Logic tìm kiếm cũng đơn giản. Nhận câu truy vấn của người dùng -> đi qua nomic-embed-text để thành vector -> truy vấn cosin với vector bài viết và sắp xếp theo khoảng cách gần nhất giữa 2 vector.
Tuy vậy, nếu người dùng tìm kiếm theo từ khoá ví dụ như node.js, javascript... thì nhiều khả năng semantic sẽ không cho ra kết quả vì dữ liệu quá ngắn, vector tạo ra không mang đủ ý, khiến cho khoảng cách cosin trở nên quá xa. Vậy nên để xử lý trường hợp này phải duy trì thêm một phong cách tìm kiếm fulltext. May mắn là Supabase cũng hỗ trợ loại tìm kiếm này.
Kể ra thì có vẻ đơn giản, tuy nhiên điều khó nhất với tôi là các bước tiền xử lý dữ liệu.
Một bài viết thường biểu đạt nhiều nội dung, có nội dung chính cả nội dung phụ hoạ. Thông thường thì người tìm kiếm chỉ quan tâm đến nội dung chính của bài viết và họ có khuynh hướng tìm kiếm những thứ liên quan đến nội dung chính. Nếu như chuyển đổi toàn bộ nội dung của bài viết thành vector sẽ làm "loãng" hoặc "nhiễu" vì kích thước của vector là có hạn. Tôi nghĩ rằng nếu loại bỏ được yếu tố phụ hoạ và tăng cường ý chính thì tìm kiếm sẽ chuẩn xác hơn. Tưởng tượng, một bài viết 1500 từ chuyển sang vector 1024 chiều, so với một bài chỉ chứa nội dung chính 500 từ cũng trong vector đó thì cái nào biểu thị "sắc nét" hơn?
Quy tắc tìm kiếm của người dùng cũng rất khó đoán vì mỗi người có cách tìm kiếm khác nhau. Có người thích ngắn gọn, cũng có người thích viết dài hoặc có người thích cung cấp cả ngữ cảnh cho câu hỏi... vì thế xử lý dữ liệu nhập vào của người dùng cũng là một thách thức, làm sao để chuyển đổi nó thành một câu hỏi ngắn gọn mà đủ ý và phù hợp với nội dung tìm kiếm trong blog.
Chất lượng mô hình AI sử dụng cũng là một vấn đề. Thường thì model nào được đào tạo nhiều thì càng tốt, cũng như model được thương mại hoá sẽ đi kèm với chất lượng. Nhưng vì giảm thiểu chi phí, hiện tại tôi đang sử dụng các mô hình LLMs miễn phí đi kèm với nhiều giới hạn. Hy vọng một ngày nào đó tôi sẽ tích hợp được các models mạnh mẽ hơn để tăng chất lượng tìm kiếm cho blog của mình.
Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.
Bình luận (0)