2015-05-20

Autocomplete and misspells/spelling corrections with Sphinx

Autocomplete and misspells/spelling corrections with Sphinx


Configuration

Default configuration file /etc/sphinx/sphinx.conf. Có 3 main blocks: index, searchd, source.
Block source chứa loại source (type of source) ở đây là mysql, thông tin kết nối username, password tới MySQL server. Column đầu tiên của SQL query là unique ID. SQL query sẽ chạy mỗi khi index và dump data vào Sphinx index file.

Ví dụ

source movielens
{
     type        = mysql

     sql_host    = localhost
     sql_user    = root
     sql_pass    = hArjRlutHk
     sql_db         = movielens
     sql_port    = 3306   # optional, default is 3306

     # Indexer query
    # document_id MUST be the very first field
    # document_id MUST be positive (non-zero, non-negative)
    # document_id MUST fit into 32 bits
    # document_id MUST be unique
     sql_query      = \
        SELECT id, title, UNIX_TIMESTAMP(release_date) AS release_date, \
        length(title) AS title_length FROM movies

     sql_attr_uint     = title_length
     sql_attr_timestamp   = release_date
     sql_field_string  = title

     # Document info query
     # ONLY used by search utility to display document information
    # MUST be able to fetch document info by its id, therefore
    # MUST contain '$id' macro
     sql_query_info    = SELECT * FROM documents WHERE id=$id
}

Block index chứa thông tin về source và đường dẫn chứa data. Thông tin charset_type nên là utf-8.

index movielens
{
     source         = movielens
    
     # mkdir -p /var/lib/sphinx/data or mkdir -p /var/data/sphinx
     # chown -R sphinx:sphinx /var/lib/sphinx/data
     path        = /var/lib/sphinx/data/movielens
     docinfo     = extern
     charset_type   = utf-8
}

Block searchd chứa thông tin về port và 1 số biến khác để chạy Sphinx daemon.

searchd
{
     listen         = 127.0.0.1:9312
     listen         = 9306:mysql41
     log            = /var/log/sphinx/searchd.log
     query_log      = /var/log/sphinx/query.log
     read_timeout   = 5
     max_children   = 30
     pid_file    = /var/run/sphinx/searchd.pid
     max_matches    = 1000
     seamless_rotate   = 1
     preopen_indexes   = 1
     unlink_old     = 1
     workers        = threads # for RT to work
     binlog_path    = /var/lib/sphinx
}

Sau khi chỉnh sửa configuration thực hiện index bằng command. Nếu không connect được MySQL do lỗi không tìm thấy sock ví dụ /var/lib/mysql/mysql.sock, kiểm tra sock hiện tại của MySQL và sock báo lỗi có tồn tại hay không?

# Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock'
# mkdir /var/lib/mysql
# ln -s /tmp/mysql.sock /var/lib/mysql/mysql.sock
# Run indexer for the first time not using --rotate
indexer --config /etc/sphinx/sphinx.conf --all

# Sphinx 2.0.8-id64-release (r3831)
# Copyright (c) 2001-2012, Andrew Aksyonoff
# Copyright (c) 2008-2012, Sphinx Technologies Inc (http://sphinxsearch.com)
#
# using config file '/etc/sphinx/sphinx.conf'...

Chạy indexer lần đầu tiên không có option '--rotate' để tránh báo lỗi

WARNING: index 'movielens': preload: failed to open movielens.sph: Permission denied; NOT SERVING

Để thực hiện schedule việc index thực hiện tạo cronjob.

crontab -e

# Content
@hourly /usr/bin/indexer --rotate --config /etc/sphinx/sphinx.conf --all

Start daemon

/etc/init.d/searchd start

# Starting searchd: Sphinx 2.0.8-id64-release (r3831)
# Copyright (c) 2001-2012, Andrew Aksyonoff
# Copyright (c) 2008-2012, Sphinx Technologies Inc (http://sphinxsearch.com)
#
# using config file '/etc/sphinx/sphinx.conf'...
# listening on 127.0.0.1:9312
# listening on all interfaces, port=9306
# precaching index 'movielens'
# precaching index 'testrt'                                  
# precached 2 indexes in 0.003 sec

Thực hiện test configuration

mysql -h0 -P 9306
# Welcome to the MySQL monitor.  Commands end with ; or \g.
# Your MySQL connection id is 1
# Server version: 2.0.8-id64-release (r3831)
#
# Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.
#
# Oracle is a registered trademark of Oracle Corporation and/or its
# affiliates. Other names may be trademarks of their respective
# owners.
#
# Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SELECT * FROM movielens WHERE
-> MATCH('story') LIMIT 0,3; SHOW META;

+------+--------+--------------------------------+--------------+--------------+
| id   | weight | title                          | release_date | title_length |
+------+--------+--------------------------------+--------------+--------------+
|    1 |   1663 | Toy Story (1995)               |    788893200 |           16 |
|  308 |   1663 | FairyTale: A True Story (1997) |    852051600 |           30 |
|  478 |   1663 | Philadelphia Story, The (1940) |            0 |           30 |
+------+--------+--------------------------------+--------------+--------------+
3 rows in set (0.00 sec)


Autocomplete


Note:
§  Nếu không có document_id dùng crc32(title) thay cho document_id
§  SELECT query có title_length
§  Index để min_prefix_len = 1 để index khi có space
§  Allocate đủ memory và thực hiện mlock = 1 tránh attack, access DB
§  Limit số lượng query, không cần dùng ranker trong query chỉ cần order theo title_length à option max_matches=10, ranker=none
§  Thực hiện match start of string anchor "^input*"


Misspells/Spelling corrections (suggestion)


Blog (tiếng Nga) http://habrahabr.ru/post/61807 mô tả kỹ thuật thực hiện trong một ví dụ trong thư mục misc/suggest/ của Sphinx source.
Ý tưởng cơ bản như sau so sánh sự khác nhau giữa các từ trong search query và các từ trong 1 từ điển. Các bước thực hiện như sau:
§  Xác định từ điển, có thể là từ điển thật hay từ titles, keywords của sản phẩm. Có thể phát sinh từ điển từ index có sẵn bằng cách sử dụng command line 'indexer –buildstops … –buildfreqs'
§  Có thể điều chỉnh mỗi từ trong từ điển bằng các tách thành các ký tự hoặc nhóm ký tự: bigram hay trigram (tokenize n-gram). Ví dụ tách string 'mysql' thành 'm y s q l _m my ys sq ql l_ __m mys ysq sql ql_ l__'. Trong ví dụ này underscore gạch dưới được dùng để phân tách chỉ ra giữa chữ đầu hay cuối một word với những chữ nằm giữa từ.
§  Thực hiện index dictionary và words đã modified. Có thể thêm các index attributes như word length hay bất cứ gì giúp sort kết quả. Ví dụ như tần số xuất hiện từ word frequency dùng '--buildfreqs' indexer option hay ratings của sản phẩm.
§  Thực hiện việc tách string tương tự đối với search query
§  Cuối cùng thực hiện search bằng cú pháp ".."/1 (như quorum matching operator)

Tạo dictionary từ index

indexer movielens --buildstops /home/gponster/dict.txt 100000 --buildfreqs

Sử dụng script trong Sphinx source /misc/suggest để tokenize trigram và tạo script SQL

CREATE TABLE suggest (
    id INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
    keyword VARCHAR (255) NOT NULL,
     unigrams VARCHAR (255) NOT NULL,   
     bigrams VARCHAR (255) NOT NULL,
     trigrams VARCHAR (255) NOT NULL,
    freq INTEGER NOT NULL,
     UNIQUE(keyword)
 );

SQL script

INSERT INTO suggest VALUES
 ...
 (0, 'deal', '__d _de dea eal al_ l__', 32431)
 (0, 'created', '__c _cr cre rea eat ate ted ed_ d__', 32429)
 (0, 'light', '__l _li lig igh ght ht_ t__', 32275)
 (0, 'needed', '__n _ne nee eed ede ded ed_ d__', 32252)
 (0, 'mood', '__m _mo moo ood od_ d__', 32185)
 (0, 'death', '__d _de dea eat ath th_ h__', 32140)
 (0, 'behind', '__b _be beh ehi hin ind nd_ d__', 32136)
 (0, 'usually', '__u _us usu sua ual all lly ly_ y__', 32113)
 (0, 'action', '__a _ac act cti tio ion on_ n__', 32053)
 (0, 'line', '__l _li lin ine ne_ e__', 32052)
 (0, 'pissed', '__p _pi pis iss sse sed ed_ d__', 32043)
 (0, 'bye', '__b _by bye ye_ e__', 32012)
 ...

Config index suggest

source suggest
{
     type           = mysql

     sql_host       = localhost
     sql_user       = root
     sql_pass       =
     sql_db            = movielens
     sql_port       = 3306   # optional, default is 3306

     sql_query_pre     = SET NAMES utf8
     sql_query         = SELECT id, trigrams, freq, LENGTH(keyword) AS len, keyword FROM suggest

     sql_attr_uint     = freq
     sql_attr_uint     = len
     sql_attr_string   = keyword
}


index suggest
{
     source               = suggest
     path              = /var/lib/sphinx/data/suggest
     docinfo           = extern
     charset_type         = utf-8
}

Để tăng chất lượng sugguest có thể dùng ranker=wordcount (SPH_RANK_WORDCOUNT trong API) hay ranker=proximity (SPH_RANK_PROXIMITY) để tính toán weight dựa trên xấp xỉ hay số từ khớp nhau (proximity or the number of matching words) không phải dựa trên thống kê bằng thuật toán BM25.

Ví dụ câu query cho misspell stery

"stery" => {"__s", "_st", "ste", "ter", "ery", "ry_", "y__"}

Thực hiện Sphinx search
Chiều dài correct string là 5 ± 2, thứ tự sắp xếp được tính bằng @weight + 2 - abs(5 - len)

mysql -h0 -P 9306

mysql> SELECT keyword, len, freq, @weight + 2 - abs(5 - len) AS final
FROM suggest WHERE match('@trigrams "__s _st ste ter ery ry_ y__ "/1') AND len >= 3 AND len <= 7 ORDER BY final DESC, freq DESC LIMIT 10 option ranker=wordcount;

+------+--------+------+------+---------+-------+
| id   | weight | freq | len  | keyword | final |
+------+--------+------+------+---------+-------+
|   69 |      4 |    8 |    5 | story   |     6 |
|  288 |      5 |    3 |    7 | mystery |     5 |
|  567 |      3 |    2 |    5 | steal   |     5 |
| 2155 |      3 |    1 |    5 | every   |     5 |
| 1829 |      3 |    1 |    5 | steps   |     5 |
|  957 |      3 |    1 |    5 | steel   |     5 |
| 1593 |      3 |    1 |    5 | stein   |     5 |
|  686 |      2 |    2 |    5 | stone   |     4 |
|  626 |      2 |    2 |    5 | stand   |     4 |
|  380 |      2 |    2 |    5 | glory   |     4 |
+------+--------+------+------+---------+-------+
10 rows in set (0.00 sec)

Có thể thấy hai kết quả story và mystery là khả quan nhất với weight là 4 và 5 sau khi điều chỉnh theo len là 6 và 5.

Có thể sort theo weight của full-text search hoặc sắp xếp theo độ khác nhau giữa misspell và suggestion (ít khác biệt hơn thì tốt hơn).
Nên giới hạn chiều dài (nhỏ nhất và lớn nhất) để cho ra kết quá không quá dài và không quá ngắn. Thông thường chiều dài của query search bị sai chênh lệch với chiều dài của string đúng chỉ khoảng từ 2-3 ký tự.
Để tăng thêm độ chính xác nên thực hiện thêm các bước tính toán tại ứng dụng (in application) sau khi có kết quả từ Sphinx search. Cách đơn giản nhất là tính toán khoảng cách Levenshtein của 10 từ đầu tiên mà Sphinx suggest.

Note:
N-gram là kỹ thuật tokenize một string thành các substring, thông qua việc chia đều string đã có thành các substring đều nhau, có độ dài là N. Về cơ bản thì N thường nằm từ 1-3, với các tên gọi tương ứng là unigram (n=1), bigram (n=2), trigram(n=3). Ví dụ đơn giản là chúng ta có string 'good morning', được phân tích thành bigram như sau:
"good morning" => {"go", "oo", "od", "d ", " m", "mo", "or", "rn", "ni", "in", "ng"}
Một kỹ thuật tokenize khác là morphological analysis (không sử dụng ở đây phân tích string dựa trên cấu trúc từ)

Khoảng cách Levenshtein (source wiki)
Khoảng cách Levenshtein thể hiện khoảng cách khác biệt giữa 2 chuỗi kí tự. Khoảng cách Levenshtein giữa chuỗi S và chuỗi T là số bước ít nhất biến chuỗi S thành chuỗi T thông qua 3 phép biến đổi là
§  Xoá 1 kí tự
§  Thêm 1 kí tự
§  Thay kí tự này bằng kí tự khác

Ví dụ: Khoảng cách Levenshtein giữa 2 chuỗi "kitten" và "sitting" là 3, vì phải dùng ít nhất 3 lần biến đổi.
§  kitten -> sitten (thay "k" bằng "s")
§  sitten -> sittin (thay "e" bằng "i")
§  sittin -> sitting (thêm kí tự "g")

Misspells/Spelling corrections (suggestion)

Về cơ bản Sphinx không có sẵn built-in suggester (cả term suggester lẫn phrase suggester). Đối với misspells, việc suggest được thực hiện thông qua search với kỹ thuật n-gram bởi query không có index (query viết sai). Kết quả cho thấy trigram (n = 3) cho kết quả kiểm tra từ tốt nhất (unigram không có nhiều ý nghĩa, bigram (hay bigram kết hợp trigram) sẽ ra nhiều kết quả không mong muốn khi misspell nhất là khi sai chính tả của tiếng Việt không dấu sẽ ra nhiều kết quả của tiếng Anh).
Blog (tiếng Nga) http://habrahabr.ru/post/61807 mô tả kỹ thuật thực hiện của ví dụ trong thư mục misc/suggest/ của Sphinx source.

Ý tưởng cơ bản như sau so sánh sự khác nhau giữa các từ trong search query và các từ trong 1 từ điển. Các bước thực hiện như sau:

§  Xác định từ điển, có thể là từ điển thật hay từ titles, keywords của sản phẩm. Có thể phát sinh từ điển từ index có sẵn bằng cách sử dụng command line 'indexer –buildstops … –buildfreqs'
§  Có thể điều chỉnh mỗi từ trong từ điển bằng các tách thành các ký tự hoặc nhóm ký tự: unigram, bigram hay trigram (tokenize n-gram). Ví dụ tách string (unigram + bigram + trigram) 'mysql' thành 'm y s q l _m my ys sq ql l_ __m mys ysq sql ql_ l__'. Trong ví dụ này underscore gạch dưới được dùng để phân tách chỉ ra giữa chữ đầu hay cuối một word với những chữ nằm giữa từ.
§  Thực hiện index dictionary và words đã modified. Có thể thêm các index attributes như word length hay bất cứ gì giúp sort kết quả. Ví dụ như tần số xuất hiện từ word frequency dùng '--buildfreqs' indexer option hay ratings của sản phẩm.
§  Thực hiện việc tách string tương tự đối với search query
§  Cuối cùng thực hiện search bằng cú pháp ".."/1 (như quorum matching operator)

Tạo dictionary từ index

indexer movies --buildstops ~/dict.txt 100000 --buildfreqs

Sử dụng script trong Sphinx source /misc/suggest để tokenize trigram và tạo script SQL

CREATE TABLE suggest (
    id INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
    keyword VARCHAR (255) NOT NULL,
     trigrams VARCHAR (255) NOT NULL,
    freq INTEGER NOT NULL,
     UNIQUE(keyword)
 );

SQL script

INSERT INTO suggest VALUES
 ...
 (0, 'deal', '__d _de dea eal al_ l__', 32431)
 (0, 'created', '__c _cr cre rea eat ate ted ed_ d__', 32429)
 (0, 'light', '__l _li lig igh ght ht_ t__', 32275)
 (0, 'needed', '__n _ne nee eed ede ded ed_ d__', 32252)
 (0, 'mood', '__m _mo moo ood od_ d__', 32185)
 (0, 'death', '__d _de dea eat ath th_ h__', 32140)
 (0, 'behind', '__b _be beh ehi hin ind nd_ d__', 32136)
 (0, 'usually', '__u _us usu sua ual all lly ly_ y__', 32113)
 (0, 'action', '__a _ac act cti tio ion on_ n__', 32053)
 (0, 'line', '__l _li lin ine ne_ e__', 32052)
 (0, 'pissed', '__p _pi pis iss sse sed ed_ d__', 32043)
 (0, 'bye', '__b _by bye ye_ e__', 32012)
 ...

Config index suggest

source suggest
{
     type           = mysql

     sql_host       = localhost
     sql_user       = root
     sql_pass       =
     sql_db            = movies
     sql_port       = 3306   # optional, default is 3306

     sql_query_pre     = SET NAMES utf8
     sql_query         = SELECT id, trigrams, freq, LENGTH(keyword) AS len, keyword FROM suggest

     sql_attr_uint     = freq
     sql_attr_uint     = len
     sql_attr_string   = keyword
}


index suggest
{
     source               = suggest
     path              = /var/lib/sphinx/data/suggest
     docinfo           = extern
     charset_type         = utf-8
}

Để tăng chất lượng sugguest có thể dùng ranker=wordcount (SPH_RANK_WORDCOUNT trong API) hay ranker=proximity (SPH_RANK_PROXIMITY) để tính toán weight dựa trên xấp xỉ hay số từ khớp nhau (proximity or the number of matching words) không phải dựa trên thống kê bằng thuật toán BM25.

Ví dụ câu query cho misspell stery (expected story)

"stery" => {"__s", "_st", "ste", "ter", "ery", "ry_", "y__"}

Thực hiện Sphinx search với chiều dài correct string là 5 ± 2, thứ tự sắp xếp được tính bằng @weight + 2 - abs(5 - len)
mysql -h0 -P 9306

mysql> SELECT keyword, len, freq, @weight + 2 - abs(5 - len) AS final
FROM suggest WHERE match('@trigrams "__s _st ste ter ery ry_ y__"/1') AND len >= 3 AND len <= 7 ORDER BY final DESC, freq DESC LIMIT 10 option ranker=wordcount;

+------+--------+------+------+---------+-------+
| id   | weight | freq | len  | keyword | final |
+------+--------+------+------+---------+-------+
|   69 |      4 |    8 |    5 | story   |     6 |
|  288 |      5 |    3 |    7 | mystery |     5 |
|  567 |      3 |    2 |    5 | steal   |     5 |
| 2155 |      3 |    1 |    5 | every   |     5 |
| 1829 |      3 |    1 |    5 | steps   |     5 |
|  957 |      3 |    1 |    5 | steel   |     5 |
| 1593 |      3 |    1 |    5 | stein   |     5 |
|  686 |      2 |    2 |    5 | stone   |     4 |
|  626 |      2 |    2 |    5 | stand   |     4 |
|  380 |      2 |    2 |    5 | glory   |     4 |
+------+--------+------+------+---------+-------+
10 rows in set (0.00 sec)

Có thể thấy hai kết quả story và mystery là khả quan nhất với weight là 4 và 5 sau khi điều chỉnh theo len là 6 và 5.

Có thể sort theo weight của full-text search hoặc sắp xếp theo độ khác nhau giữa misspell và suggestion (ít khác biệt hơn thì tốt hơn).
Nên giới hạn chiều dài (nhỏ nhất và lớn nhất) để cho ra kết quá không quá dài và không quá ngắn. Thông thường chiều dài của query search bị sai chênh lệch với chiều dài của string đúng chỉ khoảng từ 2-3 ký tự.

Để tăng thêm độ chính xác nên thực hiện thêm các bước tính toán tại ứng dụng (in application) sau khi có kết quả từ Sphinx search. Cách đơn giản nhất là tính toán khoảng cách Levenshtein (hỗ trợ sẵn trong PHP levenshtein) của 10 từ đầu tiên mà Sphinx suggest.

N-gram là kỹ thuật tokenize (split) một string thành các substring, thông qua việc chia đều string đã có thành các substring đều nhau, có độ dài là N. Về cơ bản thì N thường nằm từ 1-3, với các tên gọi tương ứng là unigram (n = 1), bigram (n = 2), trigram(n = 3). Ví dụ đơn string 'good morning', được phân tích thành bigram như sau:

"good morning" => {"go", "oo", "od", "d ", " m", "mo", "or", "rn", "ni", "in", "ng"}

Một kỹ thuật tokenize khác là morphological analysis (không sử dụng ở đây phân tích string dựa trên cấu trúc từ)

Khoảng cách Levenshtein (source wiki)
Khoảng cách Levenshtein thể hiện khoảng cách khác biệt giữa 2 chuỗi kí tự. Khoảng cách Levenshtein giữa chuỗi S và chuỗi T là số bước ít nhất biến chuỗi S thành chuỗi T thông qua 3 phép biến đổi là
§  Xoá 1 kí tự
§  Thêm 1 kí tự
§  Thay kí tự này bằng kí tự khác
Ví dụ: Khoảng cách Levenshtein giữa 2 chuỗi "kitten" và "sitting" là 3, vì phải dùng ít nhất 3 lần biến đổi.
§  kitten -> sitten (thay "k" bằng "s")
§  sitten -> sittin (thay "e" bằng "i")
§  sittin -> sitting (thêm kí tự "g")

Cài đặt

Có thể dùng phương pháp tương tự như trên đối với phrase suggestion.
§  Không thể dùng buildstop của Sphinx đơn thuần như trong ví dụ vì buildstop dùng whitespace tokenizer à cần phrase suggester
§  Function trigram của ví dụ trên chưa thực hiện ASCII folding (bỏ dấu tiếng Việt) à search không dấu không ra
§  Dùng tokenizer tiếng Việt để tăng độ chính xác

Chuẩn bị danh sách tên diễn viên, đạo diễn:

àlex pastor
ác lôi
ái châu
álex gonzález
álvaro cervantes
álvaro guevara
ángela molina

Tương tự đối với danh sách title:

dont go breaking my heart 2
incisive great teacher
500 days of summer
009 no 1 the end of the beginning
1 nenokkadine
10 rules for sleeping around
10 things i hate about you
gia đình tinh võ
những cô gái chân dài
nữ tướng cướp
cô nàng đáng yêu
phố wall
một thời để nhớ
quán quân siêu đẳng

Đối với title tiếng Việt có thể tăng độ chính xác bằng cách dùng tool vnTokenizer để tokenize thêm cho tiếng Việt.

vnTokenizer.sh -i ./title-vn-lowercase.txt -o title-vn-output.txt
sed 's/ /\r\n/g' <./title-vn-output.txt | sed 's/_/ /g' | sed '/^.\{,7\}$/d' | sed '/^[0-9\/.+-]\+$/d' >result.txt

Kết quả sau khi sử dụng vnTokenizer.

à
á
á phiện
ác
ác chiến
ác mộng
ác nghiệt

Lọc bỏ những line là stopwords và các line có len < 7. Thực hiện insert vào DB, trong trường hợp này coi như freq = 1 với các phrase.

gia đình
nữ tướng
quán quân
siêu đẳng
hoàng hôn
oan hồn
làm nên
tĩnh lặng
cửa hàng
áo cưới
công chúa
arabela
nụ cười

Sử dụng script ngrams.py để tạo SQL script

python ngrams.py -c trigrams -i result.txt -sql

Kết quả

DROP TABLE IF EXISTS suggest;
CREATE TABLE suggest (
     id INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
     keyword  VARCHAR(255) NOT NULL,
     trigrams VARCHAR(255) NOT NULL,
     freq  INTEGER NOT NULL,
     UNIQUE(keyword)
);

INSERT INTO suggest VALUES
(0, 'gia đình', '__g _gi gia ia_ a__ __d _di din inh nh_ h__ ', 1),
(0, 'nữ tướng', '__n _nu nu_ u__ __t _tu tuo uon ong ng_ g__ ', 1),
(0, 'quán quân', '__q _qu qua uan an_ n__ __q _qu qua uan an_ n__ ', 1),
(0, 'siêu đẳng', '__s _si sie ieu eu_ u__ __d _da dan ang ng_ g__ ', 1),
(0, 'hoàng hôn', '__h _ho hoa oan ang ng_ g__ __h _ho hon on_ n__ ', 1),
(0, 'oan hồn', '__o _oa oan an_ n__ __h _ho hon on_ n__ ', 1),
(0, 'làm nên', '__l _la lam am_ m__ __n _ne nen en_ n__ ', 1),
(0, 'tĩnh lặng', '__t _ti tin inh nh_ h__ __l _la lan ang ng_ g__ ', 1),
(0, 'cửa hàng', '__c _cu cua ua_ a__ __h _ha han ang ng_ g__ ', 1),
(0, 'áo cưới', '__a _ao ao_ o__ __c _cu cuo uoi oi_ i__ ', 1),
(0, 'công chúa', '__c _co con ong ng_ g__ __c _ch chu hua ua_ a__ ', 1),



Code ngram.py cơ bản như sau:
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Notepad++ Settings->Preferences->Tab Settings->"Replace by space"
#
import os
import sys
import struct
import getopt
import unidecode
import re
import codecs
import nltk

def confirm(prompt, resp=False):
    """prompts for yes or no response from the user. Returns True for yes and
    False for no.

    'resp' should be set to the default value assumed by the caller when
    user simply types ENTER.

    >>> confirm(prompt='Create Directory?', resp=True)
    Create Directory? [y]|n:
    True
    >>> confirm(prompt='Create Directory?', resp=False)
    Create Directory? [n]|y:
    False
    >>> confirm(prompt='Create Directory?', resp=False)
    Create Directory? [n]|y: y
    True

    """

    if prompt is None:
        raise Exception('Not valid prompt')

    if resp:
        prompt = '%s %s/%s: ' % (prompt, 'Y', 'n')
    else:
        prompt = '%s %s/%s: ' % (prompt, 'N', 'y')

    while True:
        ans = raw_input(prompt)
        print ''
        if not ans:
            return resp
        if ans not in ['y', 'Y', 'n', 'N']:
            print 'please enter y or n.'
            continue
        if ans == 'y' or ans == 'Y':
            return True
        if ans == 'n' or ans == 'N':
            return False

def debug(msg, level='info'):
    print '%s : %s' %(level, msg)

def check(test, msg):
    test or exit('Error: \n'+msg)

def ascii_folding(s):
    normal = unidecode.unidecode(s)
    return normal

def ngram(input, minGram = 2, maxGram = 3):
    tokens = re.split('\s+', input)
    result = ''
            
    if minGram == 2:
        for token in tokens:
            if len(token) == 0:
                continue

            # bigram
            text = '_' + ascii_folding(token) + '_'

            for i in range(len(text) - 1):
                result +=  text[i:i + 2] + ' '
    
    if maxGram == 3:
        for token in tokens:
            if len(token) == 0:
                continue

            # trigram
            text = '__' + ascii_folding(token) + '__'

            for i in range(len(text) - 2):
                result +=  text[i:i + 3] + ' '

    return result

def process(infile, outfile, command='trigrams', sql = False):    
    debug('Reading ' + infile)
    m = 0

    lines = codecs.open(infile, 'r', encoding='utf-8').readlines()

    fo = codecs.open(outfile, 'w', 'utf-8')
    if sql:
        fo.write('DROP TABLE IF EXISTS suggest;' + '\n' + 
            'CREATE TABLE suggest (' + '\n' +
            '\tid\tINTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,' + '\n' +
            '\tkeyword\tVARCHAR(255) NOT NULL,' + '\n' +
            '\t' + command + '\tVARCHAR(255) NOT NULL,' + '\n' +
            '\tfreq\tINTEGER NOT NULL,' + '\n' +
            '\tUNIQUE(keyword)' + '\n' +
            ');' + '\n')

    for line in lines:
        line = line.splitlines()[0]
        out = ''
        if command == 'bigrams':
            out = ngram(line, 2, 2)
        elif command == 'trigrams':
            out = ngram(line, 3, 3)
        elif command == 'bi_tri_grams':
            out = ngram(line, 2, 3)

        if len(out) > 0:
            if sql:
                if m == 0:
                    fo.write('INSERT INTO suggest VALUES\n')
                else:
                    fo.write(',\n')

                fo.write("(0, '" + line + "', '" + out + "', 1)")
                m =  m + 1
                if m % 1000 == 0:
                    fo.write(';\n')
                    m = 0
            else:
                fo.write(out + '\n')

    fo.close()

if __name__ == "__main__":
    infile = ''
    outfile = ''
    command = 'trigram'
    sql = False

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hi:o:c:sql", ["help", "infile=", "outfile=", "command="])
    except getopt.GetoptError:
        print 'ngram.py -c <command> -i <infile> -o <outfile>'
        sys.exit(2)
    
    for opt, arg in opts:
        if opt == '-h':
            print 'ngram.py -c <command> -i <infile> -o <outfile>'
            sys.exit()
        elif opt in ("-i", "--infile"):
            infile = arg
        elif opt in ("-o", "--outfile"):
            outfile = arg
        elif opt in ("-c", "--command"):
            command = arg
        elif opt in ("-sql"):
            sql = True

    check(command == 'trigrams' or command == 'bigrams' or command == 'bi_tri_grams', 'Comand must be bigrams|trigrams|bi_tri_grams')

    check(os.path.exists(infile), 'Input file not exists')

    if len(outfile) == 0:   
        fn, fext = os.path.splitext(infile)
        outfile = fn + '-o' + fext
        
    if os.path.exists(outfile) and not confirm('Output file exists, overwrite?'):
        sys.exit(0)

    debug('Output file: ' + outfile)
    process(infile, outfile, command, sql)

    sys.exit(0)

No comments:

Post a Comment