Article:
Search CJK
1763
ngocdaothanh.myopenid.com 172Updated over 2 years ago |
Như bài Làm từ điển dựa trên full text search engine đề cập, search các ngôn ngữ theo hệ chữ alphabet như tiếng Anh và tiếng Việt nói chung không có vấn đề vì việc phân tách các từ rất dễ dàng, chỉ cần dựa vào khoảng trắng. Với các ngôn ngữ không có khoảng trắng thì khó hơn.
Bài viết này tìm hiểu 3 giải pháp search CJK (Chinese-Japanese-Korean), nhiệm vụ thường gặp khi làm project cho khách hàng đến từ 3 nước này, cho chương trình Rails dựa trên 3 engine: Sphinx, Lucene, và Ferret thông qua 3 plugin: Thinking Sphinx, acts_as_solr, và acts_as_ferret.
Cụ thể, chúng ta viết thử chương trình Rails từ điển Anh-Nhật cực đơn giản dựa trên dữ liệu EDICT. Dữ liệu này gồm 133422 mục từ, ở dạng plain text chưa nén kích thước khoảng 9 MB, đủ lớn để đánh giá tốc độ và chất lượng search của từng giải pháp.
Khi chọn plugin đối với từng engine, ta theo hướng thực dụng và phù hợp với thực tế làm project nhất là chọn plugin dễ sử dụng nhất.
Mỗi mục từ trong tập tin dữ liệu plain text có dạng như sau:
2進数字 [2しんすうじ] /(n) (comp) binary digit/
Dữ liệu gốc là tập tin plain text theo encoding EUC-JP. Chương trình Rails cần encoding UTF-8 nên ta chuyển nó sang encoding UFT-8 bằng lệnh iconv -f euc-jp -t utf8 <tên tập tin>.
Insert dữ liệu plain text vào SQL DB
Cấu trúc bảng trong SQL DB như sau:
create_table :dict_ja_en do |t|
t.string :entry
t.string :pronunciation
t.text :description
end
Insert dữ liệu vào DB theo kiểu insert từng mục từ một là cách dễ nghĩ ra nhất nhưng lâu nhất, mất khoảng 2 phút:
source.each_line do |l|
next unless l =~ /^(.+)\s\[(.+)\]\s\/(.+)\/$/
DictJaEn.create(:entry =>$1, :pronunciation => $2, :description => $3)end
Nhanh hơn chút bằng cách dùng trực tiếp kết nối với DB server không thông qua ActiveRecord:
con = DictJaEn.connectionsource.each_line do |l|
next unless l =~ /^(.+)\s\[(.+)\]\s\/(.+)\/$/
e, p, d = con.quote($1), con.quote($2), con.quote($3)con.execute("INSERT INTO dict_ja_en(entry, pronunciation, description) VALUES(#{e}, #{p}, #{d})
end
Cách nhanh nhất, chỉ mất khoảng 10 giây, là chỉ dùng một lệnh để insert toàn bộ:
con = DictJaEn.connection
values_array = []
source.each_line do |l|
next unless l =~ /^(.+)\s\[(.+)\]\s\/(.+)\/$/
e, p, d = con.quote($1), con.quote($2), con.quote($3)
values_array << "(#{e}, #{p}, #{d})"
end
values_string = values_array.join(', ')con.execute("INSERT INTO dict_ja_en(entry, pronunciation, description) VALUES #{values_string}")
Sphinx và Thinking Sphinx
Hiện tại, bí quyết để Sphinx search CJK là chỉ định min_word_len = 1, ngram_len = 1, còn ngram_chars và charset_table như hướng dẫn ở trang wiki Unicode Character Set Tables. Tài liệu về các cấu hình trên cho thấy hỗ trợ CJK của Sphinx còn sơ khai vì chỉ tách bừa từng kí tự CJK.
Làm theo hướng dẫn sử dụng của Thinking Sphinx để hoàn thành chương trình và chạy thử, ta nhận xét:
- Tạo index cho toàn bộ 9 MB dữ liệu chỉ mất khoảng 2 giây.
- Kết quả search ra hơi nhiều và chứa cả kết quả sai bét. Ví dụ search "たべる" ra 20 kết quả, chứa cả những thứ như 肩を並べる [かたをならべる].
Để cải thiện, ta dùng mẹo sau:
- Khi index, đặt độ ưu tiên theo thứ tự: entry > pronunciation > description.
- Sắp xếp kết quả search theo thứ tự: chiều dài entry > pronunciation > entry.
Source code: sphinx.yml, dict_ja_en.rb.
Lucene và acts_as_solr
acts_as_solr dùng Solr dùng Lucene. Làm theo hướng dẫn của acts_as_solr và sửa schema.xml để dùng Lucene dùng CJKAnalyzer như sau:
<fieldType name="text" class="solr.TextField">
<analyzer type="index" class="org.apache.lucene.analysis.cjk.CJKAnalyzer">
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
<filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.EnglishPorterFilterFactory" protected="protwords.txt"/>
<filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
</analyzer>
<analyzer type="query" class="org.apache.lucene.analysis.cjk.CJKAnalyzer">
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
<filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="0" catenateNumbers="0" catenateAll="0"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.EnglishPorterFilterFactory" protected="protwords.txt"/>
<filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
</analyzer>
</fieldType>
Bật script/console rồi gõ lệnh: DictJaEn.rebuild_solr_index để tạo index (mất hơn 1 tiếng!). Kết quả search rất tốt, tốt nhất trong 3 giải pháp nhưng:
- Thời gian khởi tạo index hơi bị lâu, cả tiếng đồng hồ cho hơn 100 ngàn mục từ (trong lúc này, muốn biết Solr đang chạy đến đâu, đã index được bao nhiêu mục từ thì vào http://127.0.0.1:8982/solr). Với project ban đầu chẳng có dữ liệu gì, từ từ nhét dữ liệu vào thì đây không phải vấn đề.
- Phải cài Java. Đa số admin rất ghét cài Java lên server của mình vì nó ngốn nhiều bộ nhớ.
Ferret và acts_as_ferret
Ferret là "hàng nhái" Lucene viết bằng C cho Ruby. Do đó cách dùng Ferret và acts_as_ferret rất giống Lucene và acts_as_solr.
Cài acts_as_ferret và MultiLingualFerretTools như hướng dẫn ở trang wiki của nó là xong. Nhận xét:
- Tốc độ index cũng chậm như Lucene vì là hàng nhái, cùng một giuộc.
- Kết quả search tốt hơn của Sphinx nhưng kém Lucene vì MultiLingualFerretTools chỉ đơn giản dùng biểu thức chính qui.
Kết luận
- Tốc độ: Sphinx >> Lucene ~ Ferret
- Chất lượng: Lucene > Ferret > Sphinx
- Sphinx chọc thẳng vào SQL DB nên tốc độ cực cao nhưng tất cả dữ liệu để index phải nằm trong DB (có thể biến đổi dữ liệu qua vài hàm SQL đơn giản). Hiện (tháng 2 2009) Sphinx chỉ hoạt động với MySQL và PostgreSQL.
- Lucence và Ferret tốc độ chậm hơn nhiều nhưng uyển chuyển hơn vì có thể đút cho nó bất kì nguồn dữ liệu nào. Do đó có thể dùng chúng để tạo index cho dữ liệu không nằm trong SQL DB, như trang web bất kì trên mạng.
172

over 2 years ago