全文検索エンジンってどれがいい?→ElasticsearchとNode.jsで作るといい感じ

2016/10/15

Behrooz Kamali

0

Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

検索エンジンライブラリーとして人気の「Elasticsearch」をNode.jsから使う方法を紹介。サイト内検索など、全文検索エンジンがつくれます。

Elasticsearchは、高い性能と分散型アーキテクチャで人気のオープンソース検索エンジンです。本記事では、主要な機能およびNode.js検索エンジン作成に使用するプロセスについて順を追って説明します。

Elasticsearch入門

Elasticsearchは、Apache Luceneをベースにした高性能テキスト検索エンジンライブラリーです。Elasticsearchではデータの格納と検索ができますが、主な用途はデータベースではなく、インデックス作成、検索、データに関するリアルタイム統計情報の提供を目標とする検索エンジン(サーバー)です。

Elasticsearchは、複数のノードを追加し別のハードウェアを利用して水平方向のスケーリングを可能にする、分散型アーキテクチャを採用しています。ペタバイトのデータを処理する数千のノードをサポートし、水平方向のスケーリングはノードに障害が発生してもデータをリバランスすることで高可用性を実現します。

インポートされたデータは、検索ですぐに利用できます。Elasticsearchはスキーマフリーで、JSON文書にデータを格納し、データのタイプと構造を自動的に検出します。

Elasticsearchは完全なAPI駆動型で、HTTP経由でJSONデータを使うシンプルなRESTful APIを介し、ほぼすべての操作ができます。Node.js.を含むほぼすべてのプログラミング言語のクライアントライブラリーがあります。本記事では、公式クライアントライブラリーを使用します。

Elasticsearchのハードウェアとソフトウェアの要件はとても柔軟です。推奨されるプロダクション環境は64ギガバイトのメモリと、できるだけ多くのCPUコアです。リソースが制約されたシステムでも実行でき、十分なパフォーマンスを得られます(データセットが巨大ではない場合を仮定しています)。本記事の例なら、2GBのメモリーと単一のCPUコアのシステムで十分です。

Elasticsearchはすべての主要なオペレーティングシステム(Linux、OS X、Windows)で実行できます。実行にはJava Runtime Environmentの最新バージョンが必要です(「Elasticsearchのインストール」を参照)。本記事の例を実行するにはNode.js(v0.11.0以降)とnpmのインストールも必要です。

Elasticsearchの用語

Elasticsearchでは、一般的なデータベースシステムとは異なる独自用語を使用することがあります。Elasticsearchの用語と意味を説明します。

  • インデックス(Index):インデックスはElasticsearchコンテキストでは2つの意味がある。1つはデータを追加する操作のことで、データが追加されるとテキストがトークン(例えば単語)に分解され、すべてのトークンが索引付けされる。一方で、インデックスは索引付けされたデータが格納される場所を指す。基本的にはデータをインポートするとインデックスに索引付けされる。データ上で操作を実行したいときは毎回インデックス名を指定する必要がある
  • タイプ(Type):Elasticsearchにはインデックス内の文書に詳細分類があり、タイプと呼ばれている。インデックス内のすべての文書にはタイプも必要だ。たとえば、libraryインデックスを定義し、articlebookreportpresentationなど数種のデータに索引を付ける。インデックスはほぼオーバーヘッドが固定されているので、インデックスを少なく、タイプを多くすることを勧める
  • サーチ(Search):この用語は多くの人が考えるとおりの意味だ。さまざまなインデックスとタイプのデータを検索できる。Elasticsearchには用語、フレーズ、ターム、ファジー、さらには地理データなど、たくさんの種類のクエリがある
  • フィルター(Filter):Elasticsearchでは、結果をさらに絞り込むために、異なる基準で検索結果をフィルターできる。文書セットに新しい検索クエリを追加する場合は関連性に基づいて順序が変わることがあるが、フィルターとして同じクエリを追加する場合は順序の変更はない
  • 集計(Aggregations):集計データ上で最小値、最大値、平均値、合計、ヒストグラムなど異なる種類の統計を提供する
  • 補完(Suggestions):Elasticsearchは入力テキストに、用語やフレーズをベースにしたさまざまな補完を用意している

Elasticsearchのインストール

ElasticsearchはApache2ライセンスで提供されており、無料でダウンロード、使用、編集ができます。Elasticsearchをインストールする前に、コンピューターにJava Runtime Environment(JRE)がインストールされているか確認してください。ElasticsearchはJavaで記述されており、実行はJavaライブラリーに依存しています。システムにJavaがインストールされているか確認するには、コマンドラインに次のように入力します。

java -version

Javaの最新安定版の使用をお勧めします(本記事の作成時点で1.8です)。システムへのJavaインストールのガイドは、ここにあります。

Elasticsearchの最新バージョン(本記事の作成時点で2.4.0)をダウンロードするには、ダウンロードページでZIPファイルをダウンロードしてください。Elasticsearchはインストール不要で、サポートされているすべてのOS用の実行ファイルの完全なセットが単一のzipファイルにまとめられています。ダウンロードしたファイルを解凍すれば完了です。TARファイルや異なるLinuxディストリビューションのパッケージの取得など、ほかにもElasticsearchを実行する方法がいくつかあります(こちらを参照してください)。

OS XにHomebrewをインストールしている場合はbrew install elasticsearchを使用してElasticsearchをインストールできます。Homebrewは実行ファイルをパスへ自動的に追加し、必要なサービスをインストールします。コマンドbrew upgrade elasticsearchだけでアプリケーションの更新もできます。

WindowsでElasticsearchを実行するには、解凍したディレクトリでコマンドラインのbin\elasticsearch.batを実行します。その他のOSの場合はターミナルで./bin/elasticsearchを実行します。この時点ではシステム上で実行します。

前述のとおり、Elasticsearchで実行できるほとんどすべての操作はRESTful APIを介します。Elasticsearchはポート9200をデフォルトで使用しています。正しく実行していることを確認するには、ブラウザーでhttp://localhost:9200/を開くと、実行中のインスタンスについて基本的な情報が表示されます。

インストールおよびトラブルシューティングについての詳細はドキュメントを参照してください。

グラフィカルユーザインタフェース

Elasticsearchは、ほとんどの機能をREST APIを介して提供しており、グラフィカルユーザインタフェース(GUI)は付属していません。APIとNode.jsを使いすべての必要な操作を実行する方法で記事は進めますが、インデックスやデータ、さらには高レベル解析の視覚的情報を提供するGUIツールもあります。

Elastic社が開発したKibanaには、データのリアルタイムサマリーに加え、カスタマイズされた可視化および分析オプションがあります。Kibanaは無料で、詳しいドキュメントが用意されています。

このほかにも、elasticsearch-head、Elasticsearch GUI、さらにはChrome拡張機能のElasticSearch Toolboxなど、コミュニティが開発したツールがあります。いずれも、インデックスやデータをブラウザーで探索したり別の検索や集計クエリを試すにも役立ちます。すべてのツールにはインストールおよび使用方法のチュートリアルが用意されています。

Node.js環境の構築

ElasticsearchはNode.jsの公式モジュールelasticsearchを提供しています。プロジェクトフォルダーにモジュールを追加し、あとで使用するときのために依存オブジェクトを保存します。

npm install elasticsearch --save

そして、以下のようにスクリプトにモジュールをインポートします。

const elasticsearch = require('elasticsearch');

最後に、Elasticsearchとの通信を処理するクライアントを設定します。この記事ではIPアドレス127.0.0.1とポート9200(デフォルト設定)を使用しているローカルマシンでElasticsearchを実行していることを前提とします。

const esClient = new elasticsearch.Client({
  host: '127.0.0.1:9200',
  log: 'error'
});

logオプションはすべてのエラーを記録します。本記事の後半では、Elasticsearchと通信するために同じesClientオブジェクトを使用します。ノードモジュールの完全なドキュメントはここにあります。

注意:本記事のソースコードはすべてGitHubにあります。簡単に理解したいなら、使用しているPCにRepoをインストールして、ソースコードを実行してください。

git clone https://github.com:sitepoint-editors/node-elasticsearch-tutorial.git
cd node-elasticsearch-tutorial
npm install

データのインポート

本記事では、ランダムに生成されたコンテンツを持つ学術論文のデータセットを使用します。データはJSON形式で、データセット内の記事数は1000です。データがどのように表示されるかは、次に紹介するデータセットの1項目を参照してください。

{
    "_id": "57508457f482c3a68c0a8ab3",
    "title": "Nostrud anim proident cillum non.",
    "journal": "qui ea",
    "volume": 54,
    "number": 11,
    "pages": "109-117",
    "year": 2014,
    "authors": [
      {
        "firstname": "Allyson",
        "lastname": "Ellison",
        "institution": "Ronbert",
        "email": "Allyson@Ronbert.tv"
      },
      ...
    ],
    "abstract": "Do occaecat reprehenderit dolore ...",
    "link": "http://mollit.us/57508457f482c3a68c0a8ab3.pdf",
    "keywords": [
      "sunt",
      "fugiat",
      ...
    ],
    "body": "removed to save space"
  }

フィールド名は一目で分かります。注意すべき唯一のポイントは、完全かつランダムに生成された記事(段落数100~200)が含まれるため、ここではbodyフィールドが表示されないことです。完全なデータセットはこちらにあります。

Elasticsearchには、単一データポイントの索引付け更新削除の方法がありますが、データのインポートには、大規模データセットの操作をより効果的に実行するのに使用される、Elasticserchのbulkメソッドを使います。

// index.js

const bulkIndex = function bulkIndex(index, type, data) {
  let bulkBody = [];

  data.forEach(item => {
    bulkBody.push({
      index: {
        _index: index,
        _type: type,
        _id: item.id
      }
    });

    bulkBody.push(item);
  });

  esClient.bulk({body: bulkBody})
  .then(response => {
    console.log('here');
    let errorCount = 0;
    response.items.forEach(item => {
      if (item.index && item.index.error) {
        console.log(++errorCount, item.index.error);
      }
    });
    console.log(
      `Successfully indexed ${data.length - errorCount}
       out of ${data.length} items`
    );
  })
  .catch(console.err);
};

const test = function test() {
  const articlesRaw = fs.readFileSync('data.json');
  bulkIndex('library', 'article', articles);
};

例では、インデックス名をlibrary、タイプをarticleとし、インデックスしたいJSONデータに渡すbulkIndex関数を呼び出します。bulkIndex関数は、esClientオブジェクト上のbulkメソッドを順に呼び出します。このメソッドは、bodyプロパティを持つオブジェクトを引数として受け取ります。

bodyプロパティに指定された値は、各操作に2つのエントリーがある配列です。最初のエントリーの操作タイプはJSONオブジェクトと指定されています。このオブジェクト内では、indexプロパティは実行する操作(今回はドキュメントのインデックス作成)だけでなく、インデックス名、タイプ名、ドキュメントIDを決定します。次のエントリーはドキュメント自体に相当します。

将来的には、この方法で同じインデックスに(本やレポートなど)ほかのタイプのドキュメントを追加する可能性に注意してください。また、各ドキュメントには任意で固有のIDを割り当てられます。固有のIDを割り当てない場合は、Elasticsearchがランダム生成したIDを割り当てます。

リポジトリーをクローン化している場合、プロジェクトルートから次のコマンドを実行して、Elasticsearchにデータをインポートできます。

$ node index.js
1000 items parsed from data file
Successfully indexed 1000 out of 1000 items

データが正しくインデックスされたか確認する

Elasticsearchの大きな特徴の1つは、リアルタイムに近い検索です。ドキュメントがインデックスされると、1秒以内で検索できるようになります(こちらを参照)。データがインデックスされたあと、indices.jsを実行するとインデックス情報を確認できます(ソースはここ)。

// indices.js

const indices = function indices() {
  return esClient.cat.indices({v: true})
  .then(console.log)
  .catch(err => console.error(`Error connecting to the es client: ${err}`));
};

クライアントのcatオブジェクトのメソッドは、実行中のインスタンスのさまざまな情報を提供します。indicesメソッドは、すべてのインデックス、ステータス、ドキュメント数、ディスク上でのサイズを一覧表示します。vオプションはcatメソッドからのレスポンスにヘッダーを追加します。

上のコードを実行すると、クラスタのステータスを示すカラーコードの出力が分かります。赤色は、クラスタになにか間違いがあり実行されていないことを示しています。黄色は、クラスタは実行中であるものの警告が出ていることを意味し、緑色は、すべてが順調に動作していることを意味します。

ローカルマシンで実行しているほとんどの場合、(設定に応じて)黄色のステータスが表示されます。これはデフォルトの設定にクラスタ用の5つのノードが含まれていることが理由ですが、ローカルマシンはインスタンスを1つだけ実行しているからです。実稼働環境では常に緑色のステータスを目指すべきですが、本記事では黄色のステータスのままでElasticsearchの使用を継続します。

$ node indices.js
elasticsearch indices information:
health status index   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   library   5   1       1000            0     41.2mb         41.2mb

ダイナミックマッピングとカスタムマッピング

Elasticsearchはスキーマフリーです。これは(SQLデータベースでのテーブル定義のように)データ構造を定義する必要がなく、インポートする前にElasticsearchが自動的に検出することです。ただし、スキーマフリーとされてはいるものの、データ構造上の制限がいくつかあります。

Elasticsearchはマッピングとしてデータ構造を参照します。マッピングが存在しない場合は、データがインデックスされたときにElasticsearchがJSONデータの各フィールドを調べ、タイプに基づいて自動的にマッピングを定義します。フィールドのマッピングエントリーがすでに存在する場合は、同じフォーマットで新規データが追加されているか確認します。それ以外の場合はエラーとなります。

たとえば、{"key1": 12}がすでにインデックスされている場合はElasticsearchが自動的にフィールドkey1longとしてマッピングします。するとlongになるのはフィールドタイプkey1なので、{"key1": "value1", "key2": "value2"}をインデックスしようとするとエラーが出ます。同時に、オブジェクト{"key1": 13, "key2": "value2"}はタイプstringkey2が追加されることで問題なくマッピングされます。

マッピングに関してこれ以上詳細に説明はしませんが、ほとんどの部分で自動マッピングがうまく動作することは伝えておきます。マッピングの詳しい解説は、Elasticsearchのドキュメントを読んでください。

後編に続く

※本記事はMark BrownVildan SofticMoritz Krögerが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Build a Search Engine with Node.js and Elasticsearch

[翻訳:柴田理恵/編集:Livit

Copyright © 2016, Behrooz Kamali All Rights Reserved.

Behrooz Kamali

Behrooz Kamali

MEANスタックに特化したフルスタック開発者です。オペレーションズリサーチ、データ分析、アルゴリズム設計、効率の分野の専門知識を習得して、産業およびシステムエンジニアリングの博士号も取得しました。開発をしていないときは、教えることや、新しいことを学ぶのを楽しんでいます。

Loading...