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'

No comments:

Post a Comment