Fulltext search with Node.js and elasticsearch

2012 11/18 tokyo nodefest 2012 @swdyh

Fulltext search with Node.js and elasticsearch

2012 11/18 tokyo nodefest 2012 @swdyh

swdyh (twitter / github / tumblr)

今日のスライド http://swdyh.github.com

  • ブラウザ拡張開発
  • JavaScriptやRuby、Goに興味

Fulltext search

  • システム開発に検索はつきもの
  • 小規模なら全件走査 LIKE '%query%'
  • それで間に合わなくなってくると全文検索エンジン
    • あらかじめindexを作る。本の最後にあるindex
  • Namazu, Hyper Estraier, Senna, Tritonn, Groonga, Sphinx, Lucene, Solr, elasticsearch

elasticsearch

http://www.elasticsearch.org

  • Open Souce
  • Apache Lucene
  • シンプルなRESTful API
  • 分散検索
  • ドキュメントストア (MongoDB, CouchDB)

Solrとの違い

  • Solrを知らないので分からない…
  • どちらも開発が活発
    • 機能差はなくなる傾向
  • elasticsearchは開発者の使いやすさや管理しやすさを重視
    • APIがシンプル
    • クラスタリングの手間を少なく

elasticsearch.com

利用事例

  • Restful API
  • DocumentStore
  • 日本語

RESTful API 01

  • HTTP (GET / POST / PUT / DELETE)
  • JSON (request / response)
  • http://localhost:9200/index/type/document_id
    • index: RDBMSでいうデータベース
    • type: RDBMSでいうテーブル、type内のドキュメントは同じmapping(スキーマ定義)が適用される
    • document: JSONデータ

RESTful API 02

% curl -X PUT localhost:9200/demo-index% curl -X PUT localhost:9200/demo-index/x/1 -d '    {"text": "hello world" }'% curl localhost:9200/demo-index/x/_search -d '    { "query": { "text": { "text": "hello" } } }'

DocumentStore

  • Document Oriented(MongoDB, CouchDB)
  • JSON(object, array, string, number, boolean, null)
  • date, geopoint, geoshape, ip, binary(base64), attachment(base64)
  • 検索エンジン側にデータがあると、条件付き検索やソートがしやすい

日本語

文章に区切りがないのでそれをなんとかする必要がある

  • 分かち書き: 今日 | は | 天気 | が | いい
  • Ngram (2 gram): 今日 | 日は | は天 | 気が | がい | いい

分かち書きを使う

Ngramを使う

Cluster 01

  • elasticsearchを起動したサーバを用意
  • それらを見つけられるようにする
    • 見つける方法
      • サーバ名を設定ファイルに書く
      • multicastで近くのネットワークを探す
      • aws-cloud plugin: AWSのユーザkeyとsecretからec2インスタンスを取得、elasticserachがそこから動いてるサーバを見つける

Cluster 02

  • node: elasticsearchインスタンス、ふつう1サーバ
  • shard: 分割されたデータを持つ、Luceneインスタンス
  • replica: shardの複製、フェイルオーバーとパフォーマンス

elasticsearch 2 nodes

Cluster 03

3 shards 1 replica: 1 nodeの場合、replicaはできない

elasticsearch 1 node

Cluster 04

3 shards 1 replica: nodeを追加、replicaが作成される

elasticsearch 2 nodes

Cluster 05

3 shards 1 replica: さらにnodeを追加、shardがリバランスされる

elasticsearch 2 nodes

Cluster 06

3 shards 1 replica: node3が落ちると、2nodeのときのようになる

elasticsearch 2 nodes

Cluster 07

Routing 01

  • どのshardに保存するか、どのshardから検索するか
  • routingパラメータでキーを指定する
  • 同じキーなら同じshard
  • 通常の検索は全てのshardを検索して結果を集めて返すが、routingを指定した場合はそのデータがあるshardだけを検索するので効率がいい
  • ユーザidで、ログを年月で、など。デフォルトはドキュメントのid

Routing 02

curl -X PUT localhost:9200/demo-index\     /x/1?routing=201211 -d ...curl localhost:9200/demo-index\    /x/_search?routing=201211 -d ...

Routing 03

search without routing

Routing 04

search with routing

Node.jsから使う

  • elasticsearch用モジュール(elastical, elasticsearchclient, es...)
  • http client
    • 標準のhttpクライアント
    • request (おすすめ、JSONが扱いやすい)

index作成 01

var request = require('request')request.put({    uri: 'http://localhost:9200/demo-index',    json: true}, function(err, res, val) {    console.log(err, val)})

index作成 02 (mappings: schema)

request.put({    uri: 'http://localhost:9200/demo-index',    json: { mappings: { demo: {        properties: { text: { type: 'string',                              analyzer: 'kuromoji' } } } } }}, function(err, res, val) {    console.log(err, val)})

データ追加(put)

request.put({    uri: 'http://localhost:9200/demo-index/demo/1',    json: { text: 'こんにちは世界' }}, function(err, res, val) {    console.log(err, val)})

データ追加(post)

// idが自動で付加されるrequest.post({    uri: 'http://localhost:9200/demo-index/demo',    json: { text: 'こんにちは世界' }}, function(err, res, val) {    console.log(err, val)})

検索

request({    uri: 'http://localhost:9200/demo-index/demo/_search',    json: {         query: { text: { text: '世界' } }    }}, function(err, res, val) {    console.log(err, val)})

Quereis & Filters

  • Queries: match, multimatch, bool, boosting, ids, customscore, customboostfactor, constantscore, dismax, field, filtered, flt, fltfield, fuzzy, haschild, hasparent, matchall, mlt, mltfield, prefix, querystring, range, spanfirst, spannear, spannot, spanor, spanterm, term, terms, topchildren, wildcard, nested, customfiltersscore, indices, text, geo_shape
  • Filters: and, bool, exists, ids, limit, type, geobbox, geodistance, geodistancerange, geopolygon, geoshape, haschild, hasparent, matchall, missing, not, numericrange, or, prefix, query, range, script, term, terms, nested

ya-npm-search 01

Yet Another npm Search: elasticsearchとNode.jsでつくったnpm検索
http://ya-npm-search.herokuapp.com/

ya npm search screencapture

ya-npm-search 02

  • npm search
    • 全データを同期してシーケンシャルな文字列マッチ、遅い
    • どれを選べばいいのか分からない
  • elasticearchで検索、expressでWeb UI、CLI
  • githubのスター数、フォーク数を属性に追加
  • npmの依存数、スター数
  • herokuとbonsai elasticsearch addonで運用

live coding

  • elasticsearchはlocalhostで起動済み(localhost:9200)
  • 約200件のtweets(tweets.json)をelasticsearchに入れて検索する
  • 簡単なmappingを用意済(mappings.json)
    • indexの作成
    • データの追加
    • 検索
    • ExpressでかんたんなWeb UI