Showing posts with label autocomplete. Show all posts
Showing posts with label autocomplete. Show all posts

2015-05-24

Autocomplete & phrase suggester with Elasticsearch

Đầu tiên mình sẽ nói sơ qua về Indexing và Analysis để có nền đi vào việc config ES.

Con người thường không dùng chính xác từ muốn search và đôi khi cần thực hiện chuẩn hóa từ khóa của user hay query ví dụ -ed hay -ing trong tiếng Anh. Analysis thực hiện việc này cho cả documnent lẫn câu query trước khi thực hiện search.

Việc phân tích tất cả các documents tốn rất nhiều thời gian nên thông thường thực hiện trước, quá trình này gọi là indexing đánh chỉ mục. Các tài liệu đã phân tích được lưu trữ dưới định dạng format dành riêng cho việc tìm kiếm gọi là index.

Ví dụ tài liệu có từ 'Searching' sẽ được lowercase và bỏ -ing thành 'search'. Các term đã phân tích (analyzed term) như vậy được lưu trong index. Query sau đó cũng được thực hiện tương tự, nếu trùng khớp term 'search' trong document đã được indexed thì document kết quả sẽ được trả về.

Quá trình phân tích có 3 phần: (việc parse phân tách tài liệu như html, pdf để lấy text không nằm trong phạm vi của analysis).

Giả sử đã có kết quả parse từ HTML

Building a top-notch search engine


Việc đầu tiên là character filters, ví dụ trên là html-strip bỏ các tag HTML

Building a top-notch search engine

Sau đó là tokenizer (splitter), chia tách string thành những token (thẻ, dấu). Ví dụ với standard tokenizer thì - dash coi là word boundary nên tách làm 2, trong khi whitespace chỉ quan tâm đến khoảng trắng nên sẽ không tách top-notch. Một vài phương pháp tokenizing khác như n-gram sẽ thực hiện chia đều. Giả sử dash - là chia tách từ sau khi phân tách kết quả như sau:

[Building] [a] [top] [notch] [search] [engine]

Cuối cùng token filters sẽ thực hiện các xử lý thêm trên các token, ví dụ loại bỏ các hậu tố gọi là stemming, chuyển sang chữ thường. Sau quá trình này sẽ có được:

[build] [a] [top] [notch] [search] [engine]

Kết hợp giữa tokenizer và zero hay nhiều các filter sẽ được analyzer. ES cung cấp một số các analyzer chuẩn ví dụ Standand bao gồm Standard tokenizer và các filter Standard, Lowercase và Stop token filter.

 Analyzer có thể phức tạp hơn như kiểm tra chính tả hay từ đồng nghĩa trong trường hợp tìm kiếm 'search' hay 'find'. ES cũng cung cấp 1 số các stemming algorithm như Porter Stem, Snowball, và KStem. Ngoài ra có thể tạo các custom analyzer. Ví dụ custom analyzer sau là tương đương Standard analyzer

{
   "settings": {
      "analysis": {
         "analyzer": {
            "default": {
               "type": "custom",
               "tokenizer": "standard",
               "filter": ["standard", "lowercase", "stop", "kstem"]
            }
         }
      }
   }
}

OK giờ vào phần chính.

Autocomplete & phrase suggester with Elasticsearch


Thực hiện install nhanh môi trường để test.

1.1 Install Java SDK


1.1.1 Download archive file from Oracle


cd /opt
wget link-to- jdk-7u55-linux-i586.tar.gz
tar xzf jdk-7u55-linux-i586.tar.gz
cd /opt/jdk1.7.0_55/

1.1.2 Config alternatives --config


alternatives --config java

There are 1 programs which provide 'java'.

  Selection    Command
-----------------------------------------------
*  1           /usr/lib/jvm/jre-1.6.0-openjdk/bin/java

Enter to keep the current selection[+], or type selection number: 2

1.1.3 Install alternatives -- install


alternatives --install /usr/bin/java java /opt/jdk1.7.0_55/bin/java 2

1.1.4 Check installed version


java –version

java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) Client VM (build 24.55-b03, mixed mode)
Setup environmental variables
export JAVA_HOME=/opt/jdk1.7.0_55

1.2 Installation


1.2.1 Get and unpack ES zip file


cd /usr/local/src

wget https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-1.5.1.tar.gz
tar -xvf elasticsearch-1.5.1.tar.gz
mv ./elasticsearch-1.5.1 /usr/local/elasticsearch

1.2.2 Install with yum


# Add elasticsearch.repo in /etc/yum.repos.d/

[elasticsearch-1.5]
name=Elasticsearch repository for 1.5.x packages
baseurl=http://packages.elasticsearch.org/elasticsearch/1.5/centos
gpgcheck=1
gpgkey=http://packages.elasticsearch.org/GPG-KEY-elasticsearch
enabled=1

yum install elasticsearch
chkconfig --add elasticsearch

File config của Elasticsearch /etc/elasticsearch/elasticsearch.yml

1.2.3 Config IP for remote access


# [/etc/elasticsearch/elasticsearch.yml]

# Elasticsearch, by default, binds itself to the 0.0.0.0 address, and listens
# on port [9200-9300] for HTTP traffic and on port [9300-9400] for node-to-node
#network.bind_host: ["192.168.19.137", "localhost"]
#
# Publish host
network.publish_host: 192.168.19.137

#/etc/init.d/elasticsearch restart

#
# [/etc/sysconfig/iptables]
# [nano /etc/sysconfig/iptables]
# Check firewall not block port 9200 /etc/sysconfig/iptables
...
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 9200 -m state --state NEW,ESTABLISHED -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
...

#/etc/init.d/network restart

1.3 Install plugins for ES 


Install các plugins như: Mapper Attachments Type for ES (https://github.com/elasticsearch/elasticsearch-mapper-attachments) ICU Analysis for ES (https://github.com/elasticsearch/elasticsearch-analysis-icu)

1.4 JDBC plugin for ES


1.4.1 Install JDBC plugin


# Install JDBC plugin
./bin/plugin --install jdbc --url http://xbib.org/repository/org/xbib/elasticsearch/plugin/elasticsearch-river-jdbc/1.5.0.4/elasticsearch-river-jdbc-1.5.0.4-plugin.zip

cd /usr/local/src
wget http://cdn.mysql.com/Downloads/Connector-J/mysql-connector-java-5.1.35.tar.gz
tar -xvf mysql-connector-java-5.1.35.tar.gz

cp mysql-connector-java-5.1.35/mysql-connector-java-5.1.35-bin.jar /usr/share/elasticsearch/plugins/jdbc/

/etc/init.d/elasticsearch restart

1.4.2 Create index 'sample'


curl-XPOST http://localhost:9200/sample -d '{
   "settings": {
       "analysis": {
          "tokenizer": {
              "ngram_tokenizer": {
                  "type": "nGram",
                  "min_gram": "2",
                  "max_gram": "3",
                  "token_chars": ["letter", "digit"]
              }
          },
          "analyzer": {
              "ngram_analyzer": {
                  "tokenizer": "ngram_tokenizer"
              }
          }
       }
   },
   "mappings": {
       "test": {
          "_source": {
              "enabled": true
          },
          "_all": {
              "enabled": true,
              "analyzer": "ngram_analyzer"
          },
          "properties": {
              "id": {
                  "type": "integer",
                  "index": "not_analyzed"
              },
              "name": {
                  "type": "string",
                  "index": "analyzed",
                  "analyzer": "ngram_analyzer"
              }
          }
       }
   }
}'

1.4.3 Using JDBC river List toàn bộ river


curl -XGET 'http://[host]:9200/_river/_search?q=*&pretty'

Xóa river

curl -XDELETE 'http://[host]:9200/_river/[river-name]'

Suspend và resume river

curl -XPOST 'http://[host]:9200/_river/jdbc/[river-name]/_suspend|_resume'

Tạo river mới

curl -XPUT 'http://localhost:9200/_river/[river-name]/_meta' -d '{
   "type": "jdbc",
   "jdbc": {
       "driver": "com.mysql.jdbc.Driver",
       "url": "jdbc:mysql://[host]:[port]/[db]",
       "user": "[user]",
       "password": "[password]",
       "sql": "SELECT * FROM [table]",
       "index": "[index]",
       "type": "[type]"
   }
}'

Có thể dùng options "schedule": "00 00 01 * * ?" để chạy cronjob

1.5 Updating the mappings and settings of an existing index 


Kiểm tra index _settings và type _mapping

curl -XGET 'http://[endpoint]:9200/[index]/_settings'
curl -XGET 'http://[endpoint]:9200/[index]/[type]/_mapping'

Để update index phải thực hiện _close index và sau đó _open index

# Close index
curl -XPOST 'http://[endpoint]:9200/[index]/_close'
#{
#    "acknowledged": true
#}

# Updating settings (verb = PUT)
curl -XPUT 'http://[endpoint]:9200/[index]/_settings' -d '{
   "index": {
       "analysis": {
          "filter": {
              …
          },
          "analyzer": {
              "did_you_mean": {
                  …
              },
              "autocomplete": {
                  …
              },
              "default": {
                  …
              }
          }
       }
   }
}'

# Open index again
curl -XPOST 'http://[endpoint]:9200/[index]/_open'

1.6 Autocomplete & phrase suggester 


Tutorial để thực hiện autocomplete và phrase suggester cho movie.

  • Kiểm tra settings của index hiện tại.
  • Cần tạo settings cho analysic và 2 analyzer là autocomplete và didYouMean. Giả sử index đã chứa các token như sau


[quick] [brown] [fox] [jump] [over] [lazy] [dog]

Khi user thực hiện tìm kiếm thông thường với quick, nếu query match với token trong index, document sẽ được trả về. Trong trường hợp autocomplete query không phải là một full word mà là các query như:

[q] [qu] [qui] [quic] [quick]

Việc thực hiện autocomplete có thể bằng 2 cách sau:

  • Sử dụng Prefix query
  • n-gram

1.6.1 Prefix query


Sử dụng Prefix query thực sự tìm tất cả các term trong index bắt đầu bằng query giả sử 'qu', sau đó tập hợp các query này lại và tìm kiếm theo 1 boolean query dạng như sau:

quick OR quack OR quote OR quarter

Nếu trong index có rất nhiều term thỏa mãn thì quá trình này trở nên khá khó sử dụng và nhiều khi lỗi. Do có thể match ở giữa term như ball trong baseball nên Prefix query thường đi kèm Wildcard query. Thử sử dụng Prefix query với điều chỉnh settings thử nghiệm analyzer đơn giản như sau:

curl -XPUT 'http://[endpoint]:9200/[index]/_settings' -d '{
   "index": {
       "analysis": {
          "filter": {
              "stemmer_filter": {
                  "type": "stemmer",
                  "language": "english"
              },
              "shingle_filter": {
                  "max_shingle_size": "5",
                  "min_shingle_size": "2",
                  "type": "shingle"
              },
              "stopwords_filter": {
                  "type": "stop",
                  "stopwords": ["_english_"]
              }
          },
          "analyzer": {
              "did_you_mean": {
                  "filter": ["lowercase"],
                  "char_filter": ["html_strip"],
                  "type": "custom",
                  "tokenizer": "standard"
              },
              "autocomplete": {                
                  "char_filter": ["html_strip"],
                  "type": "custom",
                  "tokenizer": "standard",
                  "filter": ["lowercase", "shingle_filter"]
              },
              "default": {
                  "char_filter": ["html_strip"],
                  "type": "custom",
                  "tokenizer": "standard",
                  "filter": ["lowercase", stopwords_filter", stemmer_filter"]
              }
          }
       }
   }
}'

Giải thích settings.

  • default - Là analyzer mặc định để search document trong index. Analyzer này sử dụng các filter như sau 
    • html_strip - loại bỏ tag HTML, decode các ký tự encode cho HTML. 
    • lowercase - bỏ case-sensitive bằng cách lower-cased. 
    • stopwords - loại bỏ các stop words, like is, a, all, an, etc. 
    • stemmer - finish cleaning text và token.
  • did_you_mean - một simple analyzer dùng lowercase filter, html_strip. 
  • autocomplete - dùng filter custom từ shingle với config min và max shingle size: shingle filter là một filter thực hiện tách từ. Test thử analyzer như sau:


curl -XGET 'http://[endpoint]:9200/[index]/_analyze?pretty&analyzer=[analyzer]&text=[input]'
Kiểm tra _mapping của type
curl -XGET 'http://[endpoint]:9200/[index]/[type]/_mapping'

# Output
{
    "[index]": {
        "mappings": {
            "[type]": {
                "properties": {
                    "actors": {
                        "type": "string"
                    },
                    "description": {
                        "type": "string"
                    },
                    "directors": {
                        "type": "string"
                    },
                    "producers": {
                        "type": "string"
                    },
                    "title_en": {
                        "type": "string"
                    },
                    "title_vn": {
                        "type": "string"
                    }
                }
            }
        }
    }
}

Giả sử cần tạo thêm properties cho mapping

  • Tạo mới did_you_mean bằng cách copy title_en, title_vn, actors, description copy did_you_mean 
  • Tạo mới autocomplete bằng cách copy title_en, title_vn, actors copy autocomplete 


Thực hiện update lại _mapping như sau

curl -XDELETE 'http://[endpoint]:9200/[index]/[type]/_mapping
curl -XPUT 'http://[endpoint]:9200/[index]/[type]/_mapping?ignore_conflicts=true' -d '{
   "[index]": {
       "properties": {
          "autocomplete": {
              "type": "string",
              "analyzer": "autocomplete"
          },
          "actors": {
              "type": "string",
              "copy_to": [   "autocomplete" ]
          },
          "description": {
              "type": "string"
          },
          "directors": {
              "type": "string",
              "copy_to": [ "autocomplete" ]
          },
          "producers": {
              "type": "string"
          },
          "title_en": {
              "type": "string",
              "copy_to": [   "autocomplete" ]
          },
          "title_vn": {
              "type": "string",
              "copy_to": [ "autocomplete" ]
          }
       }
   }
}'

Thực hiện index lại và search với pattern như sau:

curl -XPOST 'http://[endpoint]:9200/[index]/[type]/_search' -d '{
   "size": 0,
   "aggs": {
       "autocomplete": {
          "terms": {
              "size": 5,
              "field": "autocomplete",
              "order": {
                  "_count": "desc"
              },
              "include": {
                  "pattern": "termi.*"
              }
          }
       }
   },
   "query": {
       "prefix": {
           "autocomplete": {
              "value": "termi"
          }
       }
   }
}'

# Output
{
   "took": 168,
   "timed_out": false,
   "_shards": {
       "total": 5,
       "successful": 5,
       "failed": 0
   },
   "hits": {
       "total": 10,
       "max_score": 0,
       "hits": []
   },
   "aggregations": {
       "autocomplete": {
          "doc_count_error_upper_bound": 0,
          "sum_other_doc_count": 0,
          "buckets": [
              {
                  "key": "terminator",
                  "doc_count": 6
              },
              {
                  "key": "terminal",
                  "doc_count": 4
              },
              {
                  "key": "terminator the",
                  "doc_count": 4
              },
              {
                  "key": "terminator the sarah",
                  "doc_count": 4
              },
              {
                  "key": "terminator the sarah connor",
                  "doc_count": 4
              }
          ]
       }
   }
}

Test với tiếng Việt vẫn chạy tạm được. Ví dụ autocomplete cho "dò" ra kết quả tuy nhiên nếu autocomplete cho "don" lẫn "dong" đều không ra kết quả tiếng Việt » chủ yếu ra tên của các actor Hàn Quốc như Dong Gun, Dong Wook. Nếu autocomplete cho từ chỉ có của tiếng Việt ví dụ "kh" thì có kết quả tiếng Việt trả về.

1.6.2 n-gram


Các tốt hơn là tách các word thành dạng các subnet các letters như đã nói 1 cách trực tiếp. Việc thực hiện này gọi là n-gram.

[q] [qu] [qui] [quic] [quick]

Trong ES có các token và các token filter có thể thực hiện tách word thành n-gram.

1.6.3 Tokenizer vs. Token Filter 

Trong document của ES ghi n-gram tokenizer thuộc loại nGram tuy nhiên đây không phải là n-gram cần dùng vì tokenizer này chia string thành n-gram token mà không phải là word-based n-gram. Thử với config mặc định (min_gram=1 và max_gram=2).

curl -XGET 'http://[endpoint]:9200/[index]/_analyze?pretty=&analyzer=ngram&text= wx yz'

# Output
[w] [x] [ ] [y] [z] [wx] [x ] [ y] [yz]

1.6.4 NGram vs. Edge NGram 


 n-gram thực hiện chia string thành các token cho tất cả các letter trong work ví dụ min = 1, max = 2 cho brown

[b] [r] [o] [w] [n] [br] [ro] [ow] [wn]

Trong khi đó edge n-gram chỉ sinh 2 token tính từ bắt đầu work

[b] [br]

Nếu muốn sinh đủ tokens cho word brown thì phải tăng max_gram lên 5

1.6.5 Fields


Multi field của ES cho phép một file index theo những cách khác nhau.

"title": {
   "type": "string",
   "fields": {
       "raw":   { "type": "string", "index": "not_analyzed" },
       "autocomplete": { "type": "string", "analyzer": "autocomplete" }      
   }
}

Ví dụ title mặc định là full-text field sẽ được analyzed trong khi titlte.raw không analyzed và title.autocomplete dùng analyzer là autocomplete. (Xem Multi-fields).

1.6.6 search_analyzer vs. index_analyzer 


 Thông thường chỉ cần chỉ định analyzer có nghĩa là search analyzer và index_analyzer thực hiện chung một quá trình như nhau. Giả sử index_analyzer sử dụng edge n-gram với min_gram=3, max_gram=20, khi đó "elasticsearch" sẽ generate như token sau:

[ela] [elas] [elast] [elasti] [elastic] [elastics] [elasticse] [elasticsea] [elasticsear] [eleasticsearc] [elasticsearch]

Khi thực hiện search query trên field vừa tạo, nếu search chỉ với "elastic" sẽ có kết quả trả về mà không cần thực hiện edge n-gram trên query. Nếu thực hiện edged n-gram trên query sẽ ra tokens

[ela] [elas] [elast] [elasti] [elastic]

Khi đó khả năng "elastic" sẽ match với document khác ngoài document ở trên. Ví dụ "ela" sẽ match với "ela" khi index cho "elapsed" mặc dù "elapsed" hoàn toàn không có chứa elastic.

[ela] [elap] [elaps] [elapse] [elapsed]

1.6.7 n-gram size & query strategy 


Sử dụng n-gram=1 không hữu ích lắm vì hầu hết đơn ký tự (letter) đều match hết các document » min_gram=2.

1.6.8 Suggester 


Ví dụ thực hiện kiểm tra suggester như sau

{
   "suggest": {
       "did_you_mean": {
          "text": "toy stery",
          "phrase": {
              "field": "suggest",
              "highlight": {
                  "pre_tag": "<em>",
                  "post_tag": "</em>"
              }
          }
       }
   },
   "size": 0,
   "query": {
       "multi_match": {
          "query": "toy stery",
          "fields": ["title_vn^3", "title_en^3", "actors"]
       }
   }
}

# Output
{
   "took": 496,
   "timed_out": false,
   "_shards": {
       "total": 5,
       "successful": 5,
       "failed": 0
   },
   "hits": {
       "total": 43,
       "max_score": 0,
       "hits": []
   },
   "suggest": {
       " did_you_mean": [{
          "text": "toy stery",
          "offset": 0,
          "length": 9,
          "options": [{
              "text": "toy steve",
              "highlighted": "toy <em>steve</em>",
              "score": 0.00015490633
          },
          {
              "text": "toy story",
              "highlighted": "toy <em>story</em>",
              "score": 0.00009656796
          },
          {
              "text": "toy starr",
              "highlighted": "toy <em>starr</em>",
              "score": 0.00006153661
          },
          {
              "text": "toy storm",
              "highlighted": "toy <em>storm</em>",
              "score": 0.000052716518
          },
          {
              "text": "toy stacy",
              "highlighted": "toy <em>stacy</em>",
              "score": 0.00003965827
          }]
       }]
   }
}

1.7 Install vn-tokenizer 


Thực hiện install plugin Elasticsearch-analysic-vietnamese tại https://github.com/duydo/elasticsearch-analysis-vietnamese

bin/plugin --url https://dl.dropboxusercontent.com/u/1598491/elasticsearch-analysis-vietnamese-0.1.1.zip --install analysis-vietnamese

#--install analysis-vietnamese
#-> Installing analysis-vietnamese...
#Trying https://dl.dropboxusercontent.com/u/1598491/elasticsearch-analysis-#vietnamese-0.1.1.zip...

#Downloading ........................................................................#....................................................................................#....................................................................................#.......................................DONE

#Installed analysis-vietnamese into /usr/share/elasticsearch/plugins/analysis-#vietnamese

Kiểm tra vi_analyzer bằng một số tên phim tiếng Việt như sau:

curl -XGET 'http://[endpoint]:9200/[index]/_analyze?pretty=&analyzer=vi_analyzer&text=sát%20thủ%20tự%20do'

# Output
{
   "tokens": [
       {
          "token": "sát thủ",
          "start_offset": 0,
          "end_offset": 7,
          "type": "word",
          "position": 1
       },
       {
          "token": "tự do",
          "start_offset": 7,
          "end_offset": 12,
          "type": "word",
          "position": 2
       }
   ]
}

tuyển tập: thiếu lâm tự » [tuyển tập] [thiếu lâm] [tự]
đội bóng thiếu lâm » [đội] [bóng] [thiếu lâm]
gián điệp hai mang » [gián điệp] [hai] [mang]
dòng máu anh hùng » [dòng] [máu] [anh hùng]
đông phương bất bại » [đông] [phương] [bất] [bại]
đại nội thị vệ » [đại nội] [thị vệ]

Đánh giá tokenizer này chưa thực sự tốt tuy nhiên vẫn có cải tiến đáng kể so với standard. Sửa lại analyzer didYouMean dùng vi_analyzer

# Close index
curl -XPOST 'http://[endpoint]:9200/[index]/_close'
curl -XPUT 'http://[endpoint]:9200/[index]/_settings' -d '{
   "index": {
       "analysis": {
          "filter": {
              "stemmer_filter": {
                  "type": "stemmer",
                  "language": "english"
              },
              "autocomplete_filter": {
                  "max_shingle_size": "5",
                  "min_shingle_size": "2",
                  "type": "shingle"
              },
              "stopwords_filter": {
                  "type": "stop",
                  "stopwords": ["_english_"]
              },
              "ngram_filter": {
                  "type": "ngram",
                  "min_gram": 2,
                  "max_gram": 15
              }
          },
          "analyzer": {
              "did_you_mean": {
                  "filter": ["lowercase"],
                  "char_filter": ["html_strip"],
                  "type": "custom",
                  "tokenizer": "vi_tokenizer"
              },
              "autocomplete": {
                  "filter": ["lowercase", "autocomplete_filter"],
                  "char_filter": ["html_strip"],
                  "type": "custom",
                  "tokenizer": "standard"
              },
              "default": {
                  "filter": ["lowercase", "stopwords_filter", "stemmer_filter"],
                  "char_filter": ["html_strip"],
                  "type": "custom",
                  "tokenizer": "standard"
              }
          }
       }
   }
}'

# Open index
curl -XPOST 'http://[endpoint]:9200/[index]/_open'

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)