Khám phá khả năng tìm kiếm văn bản toàn văn (Full-Text Search) trong DuckDB

30 tháng 4, 2026·6 phút đọc

Bài viết này hướng dẫn cách sử dụng tiện ích mở rộng Full-Text Search (FTS) của DuckDB để tìm kiếm hiệu quả trên các tập dữ liệu lớn. Tác giả so sánh khả năng của DuckDB với Postgres và Elasticsearch, đồng thời trình bày quy trình xử lý dữ liệu email bằng Python và tinh chỉnh thuật toán xếp hạng BM25.

DuckDB đã nổi lên như một công cụ mạnh mẽ để phân tích dữ liệu tại chỗ (in-process analytics), nhờ khả năng thiết lập nguồn dữ liệu nhanh chóng và dễ dàng. Tuy nhiên, các truy vấn văn bản cơ bản đôi khi không đủ sức mạnh đối với các trường hợp sử dụng phức tạp, chẳng hạn như tìm kiếm trong kho lưu trữ lịch sử hoặc hàng ngàn email. Trong bài viết này, chúng ta sẽ cùng khám phá tính năng Tìm kiếm văn bản toàn văn (Full-Text Search - FTS) trong DuckDB, xem nó so sánh như thế nào với các giải pháp như Elasticsearch hay Postgres, và cách áp dụng vào thực tế.

Tổng quan về Full-Text Search (FTS)

FTS cho phép thực hiện các truy vấn toàn diện và có thể cấu hình cao hơn nhiều so với việc sử dụng các toán tử SQL cơ bản như =, ILIKE hay biểu thức chính quy (regex). Một trong những ưu điểm lớn của FTS là khả năng tính điểm cho kết quả tìm kiếm dựa trên các thuật toán như Okapi BM25 - thuật toán mà DuckDB hiện đang hỗ trợ.

Một số khái niệm cốt lõi trong FTS bao gồm:

  • Stemming (Cắt từ gốc): Quy trình giảm các từ về một dạng gốc chung và xử lý một số biến thể (ví dụ: walk, walks, walked đều về "walk"). Tuy nhiên, nó có thể gặp khó khăn với các dạng bất quy tắc (ví dụ: "mice" và "mouse").
  • Stop words (Từ dừng): Loại bỏ các từ phổ biến như "the", "and", "of" vì sự xuất hiện của chúng có thể làm sai lệch kết quả tìm kiếm.
  • Strip accents (Loại bỏ dấu): Chuẩn hóa các ký tự có dấu thành dạng không dấu (ví dụ: "á", "ä" thành "a").

Thuật toán BM25 có hai tham số quan trọng để tinh chỉnh điểm số:

  • k₁: Tần suất thuật ngữ (liệu việc xuất hiện nhiều lần một từ có ý nghĩa hơn không?).
  • b: Chuẩn hóa độ dài tài liệu (liệu tài liệu dài hơn có ý nghĩa hơn hay bị phạt điểm?).

Thiết lập và Cài đặt

DuckDB không cung cấp FTS ngay trong lõi (core), nhưng nó có sẵn thông qua tiện ích mở rộng (extension). Việc cài đặt rất đơn giản:

INSTALL fts;
LOAD fts;

Chuẩn bị dữ liệu: Xử lý tập tin Email

Để minh họa, chúng ta sẽ giả định bạn có một bộ sưu tập email (định dạng .eml) dung lượng vài GB và muốn tìm kiếm nội dung của chúng. DuckDB không thể nhập trực tiếp định dạng .eml, nên chúng ta cần một bước tiền xử lý.

Tác giả sử dụng Python với thư viện beautifulsoup4 để phân tích cú pháp email và trích xuất nội dung. Quy trình này sẽ tải tệp email, phân tích nội dung thân (body), các tiêu đề (headers) và metadata, sau đó xuất ra tệp JSON để DuckDB dễ dàng nhập vào.

Dưới đây là đoạn mã Python tóm tắt để xử lý email:

import email
import json
from bs4 import BeautifulSoup
from email import policy
from pathlib import Path

def html_to_text(html):
    soup = BeautifulSoup(html, "html.parser")
    for tag in soup(["script", "style", "head", "title", "meta"]):
        tag.decompose()
    return soup.get_text(" ", strip=True)

def extract_body(msg):
    try:
        for kind in ("plain", "html"):
            part = msg.get_body(preferencelist=(kind,))
            if part is None:
                continue
            try:
                content = part.get_content()
            except Exception:
                continue
            if not (content and content.strip()):
                continue
            return html_to_text(content) if kind == "html" else content
        return None
    except Exception as e:
        print(f"Couldn't parse body: {e}")
        return None

# Vòng lặp xử lý các tệp .eml và lưu thành .json
for path in Path(".").glob("*.eml"):
    try:
        with open(path, "rb") as f:
            msg = email.message_from_binary_file(f, policy=policy.default)
            body = extract_body(msg)
            if not body:
                continue
            row = {
                "body": body,
                "date": str(msg["date"]),
                "file": path.name,
                "from": str(msg["from"]),
                "subject": str(msg["subject"]),
                # ... các trường metadata khác
            }
            with open(f"{path}.json", "w") as f:
                f.write(json.dumps(row))
    except Exception as e:
        print(f"error parsing {path}: {e}")

Nhập dữ liệu và Tạo chỉ mục (Index)

Sau khi có các tệp JSON, chúng ta có thể nhập chúng vào DuckDB và tạo bảng dữ liệu:

CREATE TABLE emails AS SELECT * FROM read_json('*.eml.json');

Tiếp theo, thêm cột id nếu cần thiết và tạo chỉ mục FTS trên các cột subjectbody:

ALTER TABLE emails ADD COLUMN id INTEGER;
UPDATE emails SET id = rowid;

PRAGMA create_fts_index('emails', 'id', 'subject', 'body');

Thực hiện truy vấn và Tinh chỉnh

DuckDB cung cấp hàm match_bm25 để thực hiện tìm kiếm và tính điểm liên quan.

Truy vấn cơ bản

Để tìm các email chứa từ "talk" và loại bỏ các email giao dịch hoặc danh sách gửi thư:

SELECT id, body, fts_main_emails.match_bm25(id, 'talk') AS score
FROM emails
WHERE list_unsubscribe = ''
AND precedence NOT IN ('bulk', 'list', 'junk')
AND score IS NOT NULL
ORDER BY score DESC;

Tìm kiếm kết hợp (Conjunctive)

Sử dụng tham số conjunctive := 1 để yêu cầu tất cả các từ khóa đều phải xuất hiện:

SELECT id, body, fts_main_emails.match_bm25(id, 'detective trial', conjunctive := 1) AS score
FROM emails
WHERE score IS NOT NULL
ORDER BY score DESC;

Tinh chỉnh tham số BM25 (b và k₁)

Tham số b kiểm soát việc phạt điểm dựa trên độ dài tài liệu. Nếu b = 0, độ dài tài liệu bị bỏ qua. Nếu b = 1, các tài liệu dài (như bản tin) sẽ bị phạt điểm nặng.

Tham số k₁ kiểm soát trọng số của tần suất từ khóa.

  • Khi k₁ thấp: Điểm số chủ yếu phản ánh việc từ khóa có xuất hiện hay không.
  • Khi k₁ cao: Các tài liệu có từ khóa lặp lại nhiều lần sẽ được xếp hạng cao hơn.

Ví dụ so sánh điểm số với hai giá trị k₁ khác nhau:

SELECT file, subject,
fts_main_emails.match_bm25(id, 'budget', k := 0.3) AS k_low,
fts_main_emails.match_bm25(id, 'budget', k := 3.0) AS k_high
FROM emails
WHERE file LIKE 'demo-%'
ORDER BY k_high DESC;

Đánh giá và Kết luận

Mặc dù bộ tính năng FTS của DuckDB chưa hoàn thiện bằng Postgres hay Elasticsearch (ví dụ: thiếu tính năng làm nổi bật kết quả khớp - ts_headline), nhưng nó vẫn khá mạnh mẽ và đủ tốt cho hầu hết các trường hợp sử dụng khám phá dữ liệu.

Điểm mạnh lớn nhất là tốc độ và sự dễ dàng khi triển khai trên hầu hết mọi nguồn dữ liệu. Nếu bạn cần một giải pháp phức tạp hơn, việc chuyển dữ liệu từ DuckDB sang Postgres hoặc Elasticsearch là khá đơn giản. DuckDB chứng tỏ mình là một công cụ cực kỳ hữu ích trong giai đoạn đầu của bất kỳ dự án phân tích dữ liệu nào.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗