Vùng xanh

Cấu hình chỉ mục Manticore để tìm kiếm ngữ liệu tiếng Việt

Vậy là hai tháng giãn cách đã kết thúc, trước khi trở lại với công việc chính là chụp ảnh, anh đã biết sử dụng Python một cách sơ đẳng. Kết quả của khoá tự học là một dự án thu thập và tìm kiếm ngữ liệu.

Thu thập ngữ liệu thì dễ. Nhưng tìm kiếm thì là cả một vấn đề. Với hàng trăm nghìn, rồi hàng triệu dòng dữ liệu, việc tìm kiếm toàn văn trở thành một thách thức đối với PostgreSQL. May có bạn bè mách bảo nên anh đã biết đến và dùng thử Manticore – một máy tìm kiếm văn bản mã nguồn mở. Phần mềm này được viết lại từ Sphinx.

Do có rất ít người dùng còn tài liệu thì không thực sự chi tiết nên anh cũng phải loay hoay cả ngày mới tạo được một chỉ mục (index) dạng realtime từ dữ liệu nguồn là PostgreSQL.

Cảm giác ban đầu là rất choáng với tốc độ tìm kiếm toàn văn (so với PostgreSQL, tất nhiên). Nhưng anh cũng nhanh chóng nhận ra là cần phải cấu hình một số thứ để có thể tìm kiếm chuẩn xác.

Bảng mã kí tự (charset_table)

Ban đầu anh cấu hình là non_cjk, nhưng khi tìm tiếng Việt có dấu thì lại không chuẩn xác, vì bảng mã này quy các kí tự có dấu về thành không dấu. Ví dụ: tìm “tiền” thì cũng ra cả “tiên”.

Như vậy là phải cấu hình bảng mã kí tự tiếng Việt, nhưng Manticore lại không có sẵn. Vì thế, anh lại phải mất công thử lên thử xuống cả buổi. Đến khi xong mới biết là trên wiki của Sphinx đã có. Nhưng, về cơ bản thì phải đầy đủ như này:

charset_table = '0..9, A..Z->a..z, a..z,
U+00C0..U+00C3->U+00E0..U+00E3, U+00E0..U+00E3,
U+00C8..U+00C9->U+00E8..U+00E9, U+00E8..U+00E9,
U+00CA->U+00EA, U+00EA, U+00CC->U+00EC, U+00EC,
U+00CD->U+00ED, U+00ED,
U+00D2..U+00F2->U+00D5..U+00F5, U+00D5..U+00F5,
U+00D9->U+00F9, U+00F9, U+00DA->U+00FA,
U+00FA, U+00DD->U+00FD, U+00FD, U+0102..U+1EF9/2'

Ranh giới cụm từ (phrase boundary)

Khi tìm “văn hoá xã hội” thì Manticore đưa ra cả những kết quả là “văn hoá, xã hội”. Đấy không phải là kết quả mong muốn khi tìm kiếm ngữ liệu. Vì thế anh phải thêm tuỳ chọn phrase_boundary và kèm theo cả phrase_boundary_step khi lập chỉ mục:

phrase_boundary = '., U+002C, :, ;, ?, !, U+2026, U+2013, /, U+005C, |, (, ), [, ]'
phrase_boundary_step = '10'

Thú thực là anh không hiểu rõ giá trị của phrase_boundary_step, nhưng nếu không có nó thì tuỳ chọn phrase_boundary sẽ bị bỏ qua. Thôi cứ đặt đại là 10 vậy.

Dạng thức của từ (wordforms)

Manticore hỗ trợ quy đồng dạng thức của từ. Ví dụ, trong tiếng Anh có thể tạo một danh sách kiểu như:

walks > walk
walking > walk
walked > walk

Tất nhiên, nội dung danh sách này là do người dùng tự định nghĩa, và nó có thể lên tới hàng triệu dòng.

Tiếng Việt thì không thay đổi dạng thức của từ như tiếng Anh, nhưng lại có hiện tượng không thống nhất trong cách viết i/y hay vị trí bỏ dấu trong các vần oa, oe, uy. Vì thế, để giải quyết tình trạng này thì có thể dùng wordforms.

Việc tạo danh sách cũng rất đơn giản với một đoạn mã Python:

# Initial lists
wrong = ['qui']
right = ['quy']

#1.1. òa, òe, ùy  > oà, oè, uỳ
init = ['', 'b', 'ch', 'd', 'đ', 'g', 'h',
    'kh', 'l', 'm', 'n', 'ng', 'nh', 'p',
    'ph', 'r', 's', 't', 'th', 'tr', 'v', 'x'
]
W1 = ['òa', 'ỏa', 'õa', 'óa', 'ọa',
    'òe', 'ỏe', 'õe', 'óe', 'ọe',
    'ùy', 'ủy', 'ũy', 'úy', 'ụy',
]
W2 = ['oà', 'oả', 'oã', 'oá', 'oạ',
    'oè', 'oẻ', 'oẽ', 'oé', 'oẹ',
    'uỳ', 'uỷ', 'uỹ', 'uý', 'uỵ',
]

for c in init:
    for x in W1:
        wrong.append(c + x)

    for y in W2:
        right.append(c + y)

#1.2. qùa > quà, qui > quy
U = ['ù', 'ủ', 'ũ', 'ú', 'ụ']
V1 = ['a', 'e', 'ê', 'ơ', 'y', 'i']
V2 = ['à', 'ả', 'ã', 'á', 'ạ',
    'è', 'ẻ', 'ẽ', 'é', 'ẹ',
    'ề', 'ể', 'ễ', 'ế', 'ệ',
    'ờ', 'ở', 'ỡ', 'ớ', 'ợ',
    'ỳ', 'ỷ', 'ỹ', 'ý', 'ỵ',
    'ỳ', 'ỷ', 'ỹ', 'ý', 'ỵ',
]

for v in V1:
    for u_ in U:
        wrong.append('q' + u_ + v)

for v in V2:
    right.append('qu' + v)


#2. hy, ky, ly > hi, ki, li
init2 = ['h', 'k', 'l', 'm', 'n', 's', 't', 'v', 'x']
Y = ['y', 'ỳ', 'ỷ', 'ỹ', 'ý', 'ỵ']
I = ['i', 'ì', 'ỉ', 'ĩ', 'í', 'ị']

for c in init2:
    for z in Y:  
        wrong.append(c + z)

    for j in I:
        right.append(c + j)


#3. Print the wordforms
for k, v in enumerate(wrong):
    print(v, '>', right[k])

Anh chạy đoạn mã trên và lưu vào tập tin có tên là wordforms-vi.txt rồi khai báo đầy đủ đường dẫn khi tạo index:

wordforms = '/usr/local/var/manticore/wordforms-vi.txt'

Như vậy là với từ “hoạ sĩ” thì dù từ khoá tìm kiếm hay trong cơ sở dữ liệu nó có là “hoạ sĩ”, “họa sĩ”, “họa sỹ”, hay “hoạ sỹ” thì cũng ra cùng kết quả.

Đánh dấu (highlight) từ khoá trong kết quả tìm kiếm

Vấn đề với Manticore đã xong. Tuy nhiên, nếu cần phải đánh dấu từ khoá trong kết quả tìm kiếm thì đôi khi biểu thức chính quy sẽ rất phức tạp. Ví dụ như trong trường hợp này thì sẽ phải là:

text = regex.sub(r"(\b(họa|hoạ) (sĩ|sỹ)\b)", "<strong>\\1</strong>", text, regex.I)

Vì thế, để giảm gánh nặng xử lí, anh thống nhất cách đặt dấu cho dữ liệu đầu vào và giữ nguyên cách viết i/y.

Ngoài ra, cũng phải lưu ý là anh không cho dấu nối (-) vào tuỳ chọn phrase_boundary. Vì thực tế là cùng một từ (thường là phiên âm) sẽ có thể có dấu nối hoặc không. Ví dụ: Tìm “vắc xin” thì sẽ phải ra cả “vắc xin” và “vắc-xin”. Nhưng để đánh dấu được từ khoá thì cũng phải điều chỉnh biểu thức chính quy để có kết quả tìm kiếm như này:

Kết quả tìm kiếm từ khoá “ô-xy” và đánh dấu từ khoá khi xem toàn văn

Đại khái vậy. Dù sao anh cũng vẫn chỉ là một người chụp ảnh chân dung ở Hà Nội :v