エンジニアリング

ReactとElastic App Searchでアプリ検索を構築する方法

ユーザーが検索を行う目的は、関連性の高い結果を得ることです。しかし、結果の関連性が高いだけでは、ユーザーの心は掴めません。検索には、快適さも求められます。検索は、高速で、応答性に優れ、インテリジェントさと効果が実感できるものでなくてはなりません。

このチュートリアルでは、アプリ検索についての理解を深めるために、ReactとElastic App Search JavaScriptクライアントを使用して、フレキシブルかつ堅牢な検索エクスペリエンスを実際に構築します。最終的には、ファセットおよびURIの一部として保持される状態での並べ替え機能を備え、さまざまなnpmパッケージをリアルタイムで検索が可能であり、見栄えが良く、関連性が高く、Reactの利点を活かしたアプリを完成させます。

完成版のコードは、GitHubにあります。


お知らせ

後続の記事にて、ElasticのオープンソースライブラリであるSearch UIを使用して、さらに迅速に検索エクスペリエンスを構築する方法を紹介しています。

ぜひご覧ください


要件

このチュートリアルでは、次のものが必要になります。

  1. 最新バージョンのNode.js
  2. 最新バージョンのnpm
  3. Elastic App Search Serviceのアカウント、または有効な14日間の無料トライアル
  4. 約30分

App Searchの概要

アプリの中心はデータです。Facebookは、ユーザーの興味を引く方法で"友達"データを提示することで、社会に爆発的に浸透しました。eBayの着想は、どこよりも効率的に中古品を探して購入できる手段を提供することでした。Wikipediaの登場で、あらゆることを簡単に調べられるようになりました。

アプリの存在意義は、データの問題を解決することです。その目的を果たすうえで、検索は欠かせません。メジャーなアプリであれば、友達、商品、会話、記事を探すこと、つまり検索が重要な要素になります。データセットがより大きく、より興味深いものになるほど、アプリは人気を集めるでしょう。検索結果の関連性が高く、有益なものであればなおさらです。

Elastic App Searchは、オープンソースの分散型RESTful検索エンジンであるElasticsearchをベースにしています。開発にElastic App Searchを使用すると、プレミアムなアプリ検索のユースケースの処理に最適化された堅牢なAPIエンドポイント群にアクセスできます。

エンジンの作成

まず、App Searchのエンジンを作成します。

エンジンは、オブジェクトを取り込んでインデックス化します。オブジェクトは、友達のプロフィール、商品、Wikiページなどのデータです。App Searchに取り込まれたデータは、フレキシブルなスキーマに基づいてインデックス化され、検索に最適化されます。このインデックスをもとに、さまざまなクライアントライブラリを活用して快適な検索エクスペリエンスを組み立てることができます。

この例では、エンジンを「node-modules」と呼ぶことにします。

エンジンを作成したら、[Credentials](認証情報)ページで次の3つの情報を取得してください。

  1. ホスト識別子(先頭がhost-)
  2. プライベートAPIキー(先頭がprivate-)
  3. パブリック検索キー(先頭がsearch-)

これらを用意できたら、本記事用のプロジェクトをクローンし、ディレクトリに移動してstarterブランチをチェックアウトし、npmのインストールを実行します。

$ git clone https://github.com/swiftype/app-search-demo-react.git
$ cd react-tutorial && git checkout starter && npm install

これで、アプリを準備できました。次は、検索用にデータを用意しましょう。

インジェスト

大半のケースでは、オブジェクトの保存場所はデータベースかバックエンドAPIです。今回は例として、静的な.jsonファイルを使用することにします。本記事用のレポジトリには、init-data.jsとindex-data.jsという2つのスクリプトが格納されています。前者は、npmから整形済みのノードモジュールデータを取得するためのスクレイパーです。データは、node-modules.jsonファイルとして取得済みです。後者は、App Searchエンジンにデータをインジェストしてインデックス化するインデクサーです。

このインデクサースクリプトを実行するためには、ホスト識別子プライベートAPIキーを渡す必要があります。

$ REACT_APP_HOST_IDENTIFIER={Your Host Identifier} \
REACT_APP_API_KEY={Your Private API Key} \
npm run index-data

すぐに、オブジェクトが100個単位でApp Searchエンジンに送られ、インデックスが作成されます。

これで、新しく作成したエンジン用のダッシュボードが構築され、約9,500個のnpmパッケージがドキュメントとしてインデックス化されます。ダッシュボードのコンテンツを理解するため、データをいろいろと操作してみてください。

app_search_engine_overview.png

Reactの実装

エンジンにデータを取り込み、準備が整ったので、コアアプリの構築に着手しましょう。

$ npm start

プロジェクトディレクトリからnpmを起動し、Reactのボイラープレートを開きます。App.cssからスタイルが取り込まれるので、ニーズに合わせてカスタマイズしましょう。

さしあたっては、検索クエリを入力できる検索ボックスが必要でしょう。検索エンジンやブラウザーに馴染んでいるユーザーは、"探したいものをここに入力してください"式の便利な入力欄を求めるからです。

//App.css
...
.App-search-box {
height:40px;
width:500px;
font-size:1em;
margin-top:10px;
}

また、App Searchのアクセス認証情報を、.envファイルのような安全な場所に格納する必要もあります。

プロジェクトのルートディレクトリにこのファイルを1つ作成し、次のように入力します。

//.env
REACT_APP_HOST_IDENTIFIER={your host identifier, prefixed with host-}
REACT_APP_SEARCH_KEY={your public search key, prefixed with search-}

変数を安全な場所に隔離できたので、検索ロジックの記述を始めましょう。

検索の作成

App.jsファイルに、コアロジックを記述していきます。このファイルは、その他のスターターファイルの大半と同じく、設定不要でReactアプリをブートストラップできる便利なツールのcreate-react-appで作成されたものです。検索をテストするためのロジックを書き込む前に、Swiftype App Search JavaScriptクライアントライブラリをインストールする必要があります。

$ npm install --save swiftype-app-search-javascript

次のコードをApp.js内に記述します。このコードでは、基本的な検索を実行します。

サンプルの検索語として、"foo"をハードコーディングします。

import * as SwiftypeAppSearch from "swiftype-app-search-javascript";
const client = SwiftypeAppSearch.createClient({
hostIdentifier: process.env.REACT_APP_HOST_IDENTIFIER,
apiKey: process.env.REACT_APP_SEARCH_KEY,
engineName: "node-modules"
});
//クエリは任意の言葉で実行できますが、今回は例としてfooを使用します。
const query = "foo";
const options = {};
client.search(query, options)
.then(resultList => console.log(resultList))
.catch(error => console.log(error))

ブラウザーが更新され、console.log経由でresultList配列が作成されます。この配列の中身を確認するには、ブラウザーの開発者コンソールを開きます。クエリの"foo"を別の文字列に置き換えて、他のクエリを数回試してみてください。クエリの変更後にページを更新すると、クエリに応じて検索結果が変化するはずです。

これで、ノードモジュールの検索を行えるようになりました。

検索結果の改善

検索のためにシンプルなパターンを構築しましたが、検索結果がconsole.logの中に隠れてしまうため、あまり役に立ちません。基本的なReactスタイルと先ほどのコードを削除してから、拡張しましょう。

これから、次のものを作成していきます。

  1. responseプロパティを保持するstate変数
  2. client.searchを使用してApp Searchのクエリを実行するperformQueryメソッド。このメソッドでは、クエリ結果をresponseプロパティ内に格納します。
  3. アプリの読み込み時に1回実行されるcomponentDidMountライフサイクルフック。今回もfooでクエリを実行しますが、任意の言葉に置き換えてかまいません。
  4. 結果のデータ出力と検索結果の合計件数を保持する構造化HTML
//App.js
// ...省略
class App extends Component {
state = {
//直近のクエリ応答を保持する新しいstateプロパティ
response: null
};
componentDidMount() {
/*アプリの初回読み込み時に結果が画面に表示されるよう
次のものをcomponentDidMountで呼び出しています*/
this.performQuery("foo");
}
//クエリを実行して応答を格納するメソッド
performQuery = queryString => {
client.search(queryString, {}).then(
response => {
//以下は、応答全体を検査できるように暫定的に追加しています
console.log(response);
this.setState({ response });
},
error => {
console.log(`error: ${error}`);
}
);
};
render() {
const {response} = this.state;
if (!response) return null;
return (


Node Module Search



{/*このクエリの結果の合計件数を表示します*/}

{response.info.meta.page.total_results} Results


{/*結果を反復処理して名前と説明を表示します*/}
{response.results.map(result => (

Name: {result.getRaw("name")}


Description: {result.getRaw("description")}



))}

);
}
}
// ...省略

上記のコードを保存すると、http://localhost:3000に、27件の検索結果がおしゃれなサウンドモジュールとともに表示されます。問題が起きた場合は、コード内に2つのconsole.logをネストしているので、コンソールを確認してください。

検索ボックスの改良

現時点では、クエリに"foo"をハードコーディングしています。自由な語句で検索できれば、検索の価値は大きく高まります。素晴らしい検索エクスペリエンスはすでに開発できたので、より一般的な検索式にあわせて最適化し、関連性の高い結果を抽出する準備は整いました。まずは、手つかずの検索ボックスから始めましょう。

優れた検索ボックスを作成するため、stateにqueryStringというプロパティを追加します。また、常にqueryStringに最新の文字列を格納するため、updateQueryメソッドを作成します。また、onChangeハンドラーを利用して、検索ボックス内のテキストがユーザーによって変更されるたびにqueryStringを更新し、新しい検索をトリガーします。

Appクラス全体を次のように編集します。

//src/App.js
// ...省略
class App extends Component {
state = {
//検索ボックスの値を追跡する新しいstateプロパティ
queryString: "",
response: null
};
componentDidMount() {
//"node"用にハードコーディングしていた検索を削除
this.performQuery(this.state.queryString);
}
//ユーザーが検索ボックスに入力するたびにonChangeイベントを処理します。
updateQuery = e => {
const queryString = e.target.value;
this.setState(
{
queryString //ユーザーが入力したクエリ文字列をを保存
},
() => {
this.performQuery(queryString); //新しい検索をトリガー
}
);
};
performQuery = queryString => {
client.search(queryString, {}).then(
response => {
this.setState({
response
});
},
error => {
console.log(`error: ${error}`);
}
);
};
render() {
const {response, queryString} = this.state;
if (!response) return null;
return (


Node Module Search



{/*クエリ文字列の値とonChangeハンドラーに接続した
検索ボックス*/}

);
}
}
// ...省略

デバウンス

現在の繰り返し処理では、検索ボックス内の変更が検出されるたびに検索が実行され、システムに大きな負荷がかかる可能性があります。これを解決するため、Lodashから提供されている_debounce_関数を使いましょう。

$ npm install --save lodash

デバウンスとは、一定のミリ秒数に基づいて受信リクエスト数をレート制限する方法です。ユーザーはクエリのフレーズを考えながら入力したり、タイプミスしたり、ものすごい速さで入力したりするので、変更が検出されるたびに毎回クエリを実行する必要はありません。

Lodashのdebounce関数内でperformQueryメソッドをラッピングして、200ミリ秒のレート制限を指定します。これにより、入力のない状態で200ミリ秒が経過するまで、次の検索クエリは開始されなくなります。

//App.js
// ...省略
import { debounce } from "lodash"; // debounceをインポート
// ...省略
performQuery = debounce(queryString => {
client.search(queryString, {}).then(
response => {
this.setState({
response
});
},
error => {
console.log(`error: ${error}`);
}
);
}, 200); // 200ミリ秒を指定。
// ...省略

レート制限には、サーバーを休ませられるだけでなく、ユーザーのクエリをスムーズに実行できるというメリットもあります。これは重要なことです。快適さが今回のポイントであるからです。

次のステップ

今回の内容は、Reactを活用して上質な検索エクスペリエンスを構築する第一歩に過ぎません。ここから、やるべきことはいくつもあります。スタイルを追加したり、ファセットキュレーション関連付け調整などのApp Searchの動的機能を実装したりしてみましょう。また、Analytics API Suiteで、ユーザーの検索アクティビティに関する重要なインサイトを調べてもよいでしょう。

もっと理解を深めたい方は、masterブランチにあるREADMEで、URIベースの状態管理の作成、およびスタイル、ページ付け、絞り込み、ファセット検索の追加に関するチュートリアルもご覧ください。少しスタイルをカスタマイズするだけで、質の高い検索エクスペリエンスを構築する基盤を用意できるでしょう。

まとめ

かつては、関連性が高く有意義なアプリ検索の構築は手間のかかるものでした。Elastic App Searchを使用すれば、適切に管理された方法で、便利で調整可能な検索機能をWebアプリに組み込めます。最大のメリットは、エンジニアでも、技術にそれほど詳しくない関係者でも、洗練された直感的なダッシュボードで主な機能を管理できることです。App Searchは、14日間の無料トライアルですぐにお試しいただけます。クレジットカードは不要です。