LIFULL Creators Blog

「株式会社LIFULL(ライフル)」の社員によるブログです。

ElasticSearch + MongoDBをNode.jsで操作する

ネクストでエンジニアをやっています瀧川です。
今回は最近先輩と共同でやった小さめのWEBアプリを作った時に表題の環境を整えるところを担当したのでその紹介をしたいと思います。

同様の記事はネット上にいくつかあったのですが、僕自身これを行うときの段階では

  • node.jsで開発するのも初めてで
  • MongoDBを扱ったこともなく
  • ElasticSearchにいたっては名前を聞いたこともない

という無能っぷりだったので色々苦労しました。 そのため、備忘録ということで書かせていただいております。

ただ、本題からずれすぎるのもアレなのでここでは表題のものそれぞれについて細かく説明しません。 あくまでこれらの連携を実現させる方法についてのみ書きます。

一応、それぞれを超カンタンに説明すると

といったところです。 リンク先は各公式HP。

それでは、本題に入りたいと思います。

ElasticSearchとMongoDBを連携させる

いまさらですが、ElasticSearchは全文検索エンジンです。 なので何かを検索します。 今回、その「何か」とはMongoDBのもつスキーマなので、MongoDBのもつデータをElasticSearchのクラスタと連携する必要があるわけです。 まずはその設定を行います。

ElasticSearchのプラグインを入れる

ElasticSearchのインストールが完了しているなら、plugin というコマンドが使えるようになっているはずです。それを使って

$ plugin -install elasticsearch/elasticsearch-mapper-attachments/1.8.0
$ plugin -i com.github.richardwilly98.elasticsearch/elasticsearch-river-mongodb/1.7.0

と実行してください。 もしこのときすでにElasticSearchが起動している場合には、プラグインを認識するために再起動させてください。

補足すると、ElasticSearchにはRiverというクラスタにデータを流し込むサービスがあって、 それのmongoDB版のプラグインが river-mongodb になります。 これを使うことでMongoDBのデータをElasticSearchに流し込むことができます。

mapper-attachmentsのほうは恥ずかしながらなぜ必要なのか正確にはよくわかっていませんが、 きっと型マッピングを扱うために必要なんだと思っております。

なにはともあれ、これでElasticSearch側はmongoDBのデータを受け入れる準備ができました。
簡単!
次はMongoDB側です。

MongoDBのReplicaSetを用意する

river-mongodbプラグインはなにやらMongoDBのインスタンスがReplicaSet(レプリケーション)として起動していないとうまく動かないらしいので、 ReplicaSetの設定をし、それを起動してやる必要があります。

まずは ReplicaSet用のディレクトリ(データ保存/ログ保存)を作成します。 もちろん名前などは適宜おこのみでつけてください。

$ mkdir  /data/mongo/rs0
$ mkdir  /data/mongo/log

作成したディレクトリを使用し、ノードを起動します。

$ mongod --replSet testrep --port 27017 --dbpath /data/mongo/rs0 --logpath /data/mongo/log/rs0.log &

ちゃんと起動していることを確認するため、立ち上げたポートにブラウザでアクセスしてみます。 ローカルで作業しているならば

http://localhost:27017/

へアクセス。
アクセス先で

You are trying to access MongoDB on the native driver port. For http diagnostic access, add 1000 to the port number

と表示されていれば成功です。
このメッセージの指示どおり+1000番のポート(今回の例だと http://localhost:28017/ )へアクセスすると管理画面となります。

ここまででレプリケーションの下準備は完了しました。
レプリケーション本来の目的として考えるとノードが1つだけって意味があるのか不明ですが、 今回はElasticSearchとの連携だけが目的なのでこのまま進みます。

これでReplicaSetを動かすための下準備はできました。
あとはコンソールから設定と初期化を行います。
まずはmongoコマンドを実行。ポート番号はもちろん先ほど起動したレプリケーションのポートです。

$ mongo -port=27017

実行したらrs.initiateというコマンドで初期化します。

 > config = {_id: 'testrep', members: [{_id: 0, host: 'localhost:27017'}]};
 > rs.initiate( config );
  • _idキーは先ほど用意したreplSetの名前
  • members._idはおこのみで
  • members.hostは先ほどブラウザでアクセスしたURL という感じに読み替えてください。

最後に

> rs.status();

を実行し、特にエラーが出なければ("ok"の値が1であれば)設定完了です。

これでmongoDB側も準備が完了しました。
最後にElasticSearchのCollection, Indexとの関連付けを行います。

ElasticSearchのCollection, Indexとの関連付け

ElasticSearchの設定はcurlコマンドを使います。 僕はここでよくタイポを起こしてウオオオオオー!となることが多かったのでシェルスクリプトで書いて実行していくのがおすすめです。
ということで下記のようなシェルスクリプトを実行します。
(ElasticSearhは9200番ポートで立ち上がっている前提)

#! /bin/sh

curl -XPUT "localhost:9200/_river/test_es/_meta" -d '
{
  "type": "mongodb",
  "mongodb": {
    "db": "test_es",
    "collection": "collectionName",
    "servers": [
      { "host": "127.0.0.1", "port": 27017 }
    ],
    "options": { "secondary_read_preference": true }
  },
  "index": {
    "name": "indexName",
    "type": "typeName"
  }
}'

curlコマンドで指定しているURLの"test_es"がIndex名になります。
mongodb.db, mongodb.collection, mongodb.indexの設定は各々実際に使用する際に適したものをつけてください。

実行後、

http://localhost:9200/river/test_es/meta

にアクセスしたときに

{"_index":"_river","_type":"test_es","_id":"_meta","_version":1,"exists":true, "_source" : 
{
  "type": "mongodb",
  "mongodb": {
    "db": "testEs",
    "collection": "collectionNames",
    "servers": [
      { "host": "127.0.0.1", "port": 27017 }
    ],
    "options": { "secondary_read_preference": true }
  },
  "index": {
    "name": "indexName",
    "type": "typeName"
  }
}}

と表示されていればOKです。

これでようやく連携部分は完了です。
最後にnode.jsから操作してみましょう。

Node.jsからの操作

まずは必要なモジュールをインストールしましょう。

$ npm install mongoose
$ nom install es

名称からお察しの通り、mongooseがMongoDB、esがElasticSearch用のモジュールとなります。

まずは適当にデータを登録。(直接DBから入れろという話は置いといて)

var mongoose = require('mongoose')                                                                                                                                                         
  , Schema
  , Model
  , db
  ;

// MongoDBへアクセス                                                                                                                                                                       
db = mongoose.connect('mongodb://localhost:27017/testEs')

// Schemaの設定                                                                                                                                                                            
Schema = new mongoose.Schema({
  name: {type: String, trim: true},
  type: {type: String, trim: true}
});

// コレクションを生成                                                                                                                                                                      
Model = db.model('collectionName', Schema);

// 適当にデータを入れる                                                                                                                                                                    
var neko = new Model({
  name: "mike",
  type: "siamese"
});

// 保存                                                                                                                                                                                   
neko.save(function(err) {
  console.log(err);
});

これでエラーがでなければ、nekoをMongoDBに登録できています。

さて、いよいよこの情報をElasticSearchから取得します。

// 関連付けたindex, typeを設定
var elasticsearch = require('es')
  , config = {
      _index: 'indexName',
      _type: 'typeName'
    }
  , es = elasticsearch(config)                                                                                                                                                             
  ;

// 全文検索開始
es.search({
  query: {
    query_string : { query : "mike" }
  }
}, function (err, data) {
  console.log(data);
  console.log(err);
});

これを実行すると、下記のような結果が帰ってくるかと思います。

//出力結果
{ took: 2,
  timed_out: false,
  _shards: { total: 5, successful: 5, failed: 0 },
  hits: { total: 1, max_score: 0.23953635, hits: [ [Object] ] } }

これが確認できれば完了です。
data.hitsオブジェクトがもつ、hitsという配列(ややこしい)の中にデータが入っています。
お疲れ様でした。

// ちなみに

// もちろんtypeでも取得可能
es.search({
  query: {
    query_string : { query : "siamese" }
  }
}, function (err, data) {
  console.log(data);
  console.log(err);
});
//出力結果
{ took: 2,
  timed_out: false,
  _shards: { total: 5, successful: 5, failed: 0 },
  hits: { total: 1, max_score: 0.23953635, hits: [ [Object] ] } }

こんなかんじで、なんとか表題の連携をさせることができました。
探り探りいろんなことを試しながらこの設定をしたのでどこか抜けなどありましたらぜひご一報を。

この環境が整え終わると、すべてがJSで完結してる"風"な気持ちになれて嬉しいですね。