node.jsでCloudSearchを動かしてみる

2014.08.27

弊社ブログで既に何回かcloudsearchの紹介をしていますが、今回はaws-sdkを使ってnode.jsで動かしてみました。

動作環境

Mac OS 10.9 Node v0.10.26 ADS-SDK 1.4.16

SDKをインストール

node.jsからcloudsearchにアクセスするのにSDKをインストールします。

npm install aws-sdk

作成しましょう

では早速実装します。今回はDomainの作成、インデックスの作成、ドキュメントの登録、検索といった事を行いたいと思います。 Domain及びインデックスの作成はあえてSDKから実行しなくてもいいかと思いますが、今回はSDKを使ってDomainの作成〜ドキュメントの登録までを自動化させてみます。

認証ファイルの作成

まずcloudsearchに対してアクセスできるように認証ファイルを作成します。 config.json

{ "accessKeyId": "XXXXXXXXXXXXXX", "secretAccessKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXX", "region": "ap-northeast-1" }

アクセスキーと、シークレットキーは自身で設定してください。

Domainの作成

CloudSearchクラスのcreateDomainメソッドを使ってtestドメインを作成します。

var AWS = require('aws-sdk');
AWS.config.loadFromPath('./config.json');

var cloudsearch = new AWS.CloudSearch();
var params = {
  DomainName: 'test';
};
cloudsearch.createDomain(params, function(err, data) {
  //実行結果後の処理
});

次に作成したドメインに対してインデックスの定義を行います。 インデックスの定義はCloudSearchクラスのdefineIndexFieldメソッドを使います。まずはインデックスのパラメータの設定です。検索対象となる項目1つづずにパラメータを設定しインデックスを登録する必要があります。今回はtitleという項目を定義したいと思います。ここではフィールドのタイプをtextとしています。タイプは他にもdate,intなどがあります。またテキストのオプションとしてスキーマの設定、検索した結果のハイライト設定、検索結果対象とするか、ソートの設定などがあります。詳しくはこちらのSDKのドキュメントを参照してください。

var params = {
  DomainName: 'test',
  IndexField: {
    IndexFieldName: 'title',
    IndexFieldType: 'text',
    TextOptions: {
      AnalysisScheme: '_ja_default_',
      HighlightEnabled: false,
      ReturnEnabled: true,
      SortEnabled: true
    }
  }
};

cloudsearch.defineIndexField(params, function(err, data) {
  //実行結果後の処理
});

さてこれでインデックスの定義は終わったのですが、このままではまだドキュメントは登録できません。定義したインデックスに対してインデックス化の処理をする必要があります。インデックス化の処理はCloudSearchクラスのindexDocumentsメソッドを使います。

var params = {
  DomainName: 'test';
};
cloudsearch.indexDocuments(params, function(err, data) {
  //実行結果後の処理
});

最後にドキュメントを登録します。ドキュメントはxmlまたはjson形式のファイルになります。詳しくはこちらを参照ください。今回はxml形式で作成したいと思います。登録するデータはアマゾンの2014年上半期のKindle本ランキングのタイトルにしてみました。 [document.xml]

<batch>
  <add id="1">
    <field name="title">進撃の巨人(13)</field>
  </add>
  <add id="2">
    <field name="title">僕だけがいない街(3)</field>
  </add>
  <add id="3">
    <field name="title">天地明察 下</field>
  </add>
  <add id="4">
    <field name="title">地獄恋 LOVE in the HELL : 1</field>
  </add>
  <add id="5">
    <field name="title">新世紀エヴァンゲリオン(13) (角川コミックス・エース)</field>
  </add>
  <add id="6">
    <field name="title">マリアビートル</field>
  </add>
  <add id="7">
    <field name="title">「全聾の天才作曲家」佐村河内守は本物か</field>
  </add>
  <add id="8">
    <field name="title">キングダム 33</field>
  </add>
  <add id="9">
    <field name="title">グラスホッパー</field>
  </add>
  <add id="10">
    <field name="title">紅殻のパンドラ(3)</field>
  </add>
</batch>                                                                        

ドキュメントのアップロードはCloudSearchDomainクラスのuploadDocumentsメソッドを使います。CloudSearchDomainクラスのコンストラクタでendpointを指定しています。endpointはドメインが作成済みの場合はマネージメントコンソールから確認することが出来ます。

var fs = require('fs');

var cloudsearchdomain = new AWS.CloudSearchDomain({
  endpoint: 'doc-test-xxxxxxxxxxxxxxxxxxxxxxxxxxx.cloudsearch.amazonaws.com',
});

fs.readFile('./document.xml', 'utf8', function (err, text) {
  var params = {
    contentType: 'application/xml',
    documents: text
  };

  cloudsearchdomain.uploadDocuments(params, function(err, data) {
    //実行結果後の処理
  });
});

これでドキュメントのアップロードまでが完了です。実際に自動化をさせようとした場合はドメインの作成やインデックス化の処理にはかなりの時間がかかるため、ドメインの作成の後に直にインデックスの定義を実行してもうまくいきません。そこで定期的にドメインの状態を取得し、処理が完了したら次の処理をするといったことが必要になります。 実際に自動化したのが次になります。node.jsで作成しているため処理を順番に行うには、コールバックをネストかする必要があります。しかし処理が長くなってくるとソースが読みづらくなってしまいます。そこで今回はasyncモジュールを使ってみました。asyncについては弊社のブログで紹介していますので詳しくはこちらを参照して下さい。今回はasyncのwaterfallを使って順番にメソッドを呼び出しています。 [createCloudSearch.js]

var async = require('async');
var AWS = require('aws-sdk'); 
var fs = require('fs');

AWS.config.loadFromPath('./config.json');

var dName = 'test';
var docEndPoint = "";

var cloudsearch = new AWS.CloudSearch();

async.waterfall([

  function(callback) {
    console.log('start');
    console.log('create domain');
    createDomain(callback);
  },
  function(data, callback) {  
    console.log('create domain success');
    console.log('describe domain'); 
    describeDomain(callback);
  },
  function (data, callback) {
    docEndPoint = data.DomainStatusList[0].DocService.Endpoint;
    console.log('define indexField');
    defineIndexField(callback);
  },
  function(data, callback) {
    console.log("define field success");
    console.log("index documents");
    indexDocuments(callback);
  },
  function (data, callback) {
    console.log("index documents success");
    console.log('describe domain');      
    describeDomain(callback);
  },
  function (data, callback) {
    console.log("upload documents");
    uploadDocument(callback);
  },
  function (data, callback) {
    console.log("upload documents success");
  }
],function (err) {
  console.log(err);
}
);

function createDomain(callback) {
  var params = {
    DomainName: dName
  };
  cloudsearch.createDomain(params, function(err, data) {
    if (err) {
      callback(err);
    } else {
      callback(null, data);
    }
  });
}

function describeDomain(callback) {

  var interval = setInterval(function() {
    var params = {
       DomainNames:[dName]
    };
    cloudsearch.describeDomains(params, function(err, data) {
      if (err) {
        clearInterval(interval);
        callback(err); 
        } else {
          console.log("Processing : " + data.DomainStatusList[0].Processing);
          if (!data.DomainStatusList[0].Processing &&
               data.DomainStatusList[0].DocService.Endpoint != null ) {
            clearInterval(interval);
            callback(null, data);
          }
        }
     });
   }, 30000);
}

function defineIndexField(callback) {
  var params = {
    DomainName: dName,
    IndexField: {
      IndexFieldName: 'title',
      IndexFieldType: 'text',
      TextOptions: {
        AnalysisScheme: '_ja_default_',
        HighlightEnabled: false,
        ReturnEnabled: true,
        SortEnabled: false
      }
    }
  };

  cloudsearch.defineIndexField(params, function(err, data) {
    if (err) {
      callback(err);
    } else {
      callback(null, data);
    }
  });
}

function indexDocuments(callback) {

  var params = {
    DomainName: dName
  };

  cloudsearch.indexDocuments(params, function(err, data) {
    if (err) {
      callback(err);
    } else {
      callback(null, data);
    }
  });
}

function uploadDocument(callback) {
  var cloudsearchdomain = new AWS.CloudSearchDomain({
    endpoint: docEndPoint
  });

  fs.readFile('./document.xml', 'utf8', function (err, text) {
    var params = {
      contentType: 'application/xml',
      documents: text
    };

    cloudsearchdomain.uploadDocuments(params, function(err, data) {
      if (err) {
        callback(err);
      } else {
        callback(null, data);
      }
    });
  });
}

またドメインの作成とインデックス化の処理は時間がかかるためsetIntervalを使って定期的にドメイン状態を監視するようにしています。ドメインの状態を取得するのはCloudSearchクラスのdescribeDomainメソッドを使用しています。処理結果のProcessingの値がtrueの場合は処理中なのでこの値がfalseになるまで待機するようにしています。だたし、ドメイン作成後はProcessingがfalseになってもEndpointが取得できなかったため今回はProcessingがfalseかつEndpointがnullでなければという判定にしています。

実行してみましょう

node createCloudSearch.js 

実行して最後にupload documents successが表示されれば処理は成功です。実行が終わるのに数十分かかるかと思います。

検索しましょう

まず先ほど作成したドメインが実際に出来ているManagement Consoleから確認します。CloudSearchメニューをクリックすると testというドメインが作成されているかと思います。Searchable Documentが10になっています。これは先ほどアップロードしたドキュメントの数を表しています。登録されているドメインをクリックしてEndpointを確認します。このEndpointを設定し検索を行います。 登録したドキュメントを実際に検索してみます。検索はCloudSearchDomainクラスのsearchメソッドを使用します。今回は引数に渡した文字で検索するようにしています。 > [search.js]

var AWS = require('aws-sdk');

AWS.config.loadFromPath('./config.json');
var cloudsearchdomain = new AWS.CloudSearchDomain({
  endpoint: 'doc-test-iemtlzbnioabgfoszxl6qoex5a.ap-northeast-1.cloudsearch.amazonaws.com',
});

var queryString = "";
process.argv.forEach(function(val, index, array) {
  if (index > 1) {
    queryString = queryString + " " + val
  }
});
var params = {
  query: queryString
};

cloudsearchdomain.search(params, function(err, data) {
  if (err) {
    console.log("error  " + err);
  } else {
    if (data.hits.found > 0) {
      console.log(data.hits.hit[0]);
    } else {
      console.log('not found');
    }
  }
});

実行してみましょう

node search.js 角川

実行結果

{ id: '5',
  fields: { title: [ '新世紀エヴァンゲリオン(13) (角川コミックス・エース)' ] } }

まとめ

今回はSDKを用いてドメインの作成から検索までを行ってみました。ドメインの作成やインデックスの定義はManagement Consoleから行ったほうが簡単なのでなかなか全て自動化にするというケースは少ないかと思いますが一度作成すれば再利用出来たりもしますので、機会があればぜひ行ってみて下さい。