Reactivesearch v3でいい感じの検索SPAを30分ぐらいで作る

この記事は[Elastic Stack (Elasticsearch)その2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/elasticsearch2)の11日目の記事です。

## 記事の内容を始める前に

[去年のAdvent Calendar記事でReactivesearchの使い方を紹介した](https://qiita.com/j-yama/items/546f6b7605c9e71d0454)のですが、2019/8/28に[Reactivesearch 3.0.0がリリースされ](https://medium.appbase.io/reactive-search-3-0-ui-components-for-building-search-uis-51b926fc0d8b?gi=67e30f41aae6)、前回の記事の内容そのままでは動作しなくなってしまいました。

では、新バージョンを無視してそのまま古いバージョンを使い続けられるのかというと、Reactivesearch 2.x系は現在のメジャーバージョンであるElasticsearch 7.x系に対応していないため、真面目にアップグレードしている人たちやこれからElasticsearchを使う人であればReactivesearch 3.x系をオススメしたい状況です。

そこで、去年のAdvent Calendar記事をReactivesearch 3.xに対応する形で書き換えてみました。コード以外の内容は同じなので、既に読んだことがある方はスルーしちゃっても良いと思います。
まだ前回の記事を読んだことがない方や、これから実際にReactivesearch 3.x系を試してみようという方は是非こちらの記事を読んでいただけますと幸いです。

## tl;dr

![](https://i.imgur.com/jZCeulj.png)

こんな感じのSPAを30分ぐらいで作ります。[^1]

[^1]: ただし、検索用データセットのDLと、この記事の解説を読む時間は除きます...ずるい？

## はじめに

「[Elasticsearch](https://www.elastic.co/jp/products/elasticsearch)を使った検索WEBアプリを作りたいんだよねぇ」
『はぁ』
「データ入れたらすぐに高速な検索できちゃうんでしょ？」
『いや、すぐにという訳には...』
「あとさ、ユーザ体験を良くするためにSPAでお願いしたいんだよねぇ」
『(また突拍子もないことを言い始めたぞ...)』
「というわけで検討よろしく！検索対象にしたいデータはCSVで送っとくから」
『とりあえず検討してみます😭』

こんな時、皆さんならどうしますか？
ガッツリ画面を作って検索クエリも自前でガリガリ書いて頑張りますか？

それも一つの方法ではありますが、PoCや小さい案件であれば、もっと楽に早く実現できる方法があればそっちを使いたい感じです。
そして、この場合はそれがあります。

[React.js](https://reactjs.org/)や[Vue.js](https://vuejs.org/)のUIコンポーネントとして使える[Reactivesearch](https://opensource.appbase.io/reactivesearch/)です。
こいつは幾つかの設定を記載するだけで **Elasticsearchに対する検索クエリの組み立てや発行も自動でやってくれる** 優れものです。Elasticsearchにデータが投入済みであれば、慣れると数分で簡単な検索SPAを実装できます。

今回はSteamのゲーム検索UIを作るチュートリアルを通して、Reactivesearchの使い方を紹介してみます。

完成済みのコードはこちらのリポジトリから確認できます。

https://github.com/j-yama/reactivesearch-steam-example-v3

## Reactivesearchとは

<img width="1418" alt="reactivesearch.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264679/2bcc8bc0-743a-ae42-d033-7ee75d19d5da.png">

Reactivesearchは[Appbase.io](https://appbase.io/)が提供するReact.js/Vue.jsで使える検索UIコンポーネントです。
名前がReact.js専用っぽい響きですが、[Vue.jsでも使えます](https://medium.appbase.io/vue-js-components-for-building-search-uis-7b2a1b6fe159)。

Reactivesearchの特徴はなんと言ってもElasticsearch専用に作り込まれていることです。
Elasticsearchに投入されているデータに対して、 **検索クエリの組み立てや発行なども代わりにやってくれます** 。
つまり、これを使えばElasticsearch初学者が陥りがちな **「Elasticsearchの検索クエリを覚えるのが大変」「検索クエリをJSONで書くのがつらい」…という問題を回避** できます。

ちなみに、設定として最低限必要なのは以下の3項目ぐらいなものです。

- Elasticsearchのホスト
- 検索対象のインデックス名
- 検索対象のフィールド名

たったこれだけの項目を設定するだけで面倒なことを考えずに検索SPAが作れちゃいます。掛け値なしに最高です。

## 今回使う検索用データを投入する(10分)

では、ここからチュートリアルを進めていきます。
まずはデータセットを用意して、Elasticsearchに投入します。

### 1. 今回使うデータセットをダウンロードする
https://s3.amazonaws.com/public-service/steam-data.tar.gz

この中の`games-features.csv`が今回使用するデータセットです。
別に怪しいデータではなくて、どのように取得されたデータであるかは以下のリポジトリで確認可能です。
何なら自分で取得し直してみても良いと思います。

[GitHub - CraigKelly/steam-data: A simple data project for Steam data](https://github.com/CraigKelly/steam-data)

### 2. ElasticsearchとKibanaを起動する

Elasticsearchと[Kibana](https://www.elastic.co/jp/products/kibana)のインストール、起動の手順に関しては他の記事に譲ります。

ただし、今回ローカルで起動する場合はCORSに引っかからないよう`elasticsearch.yml`に次の設定を追加してから起動してください。

```
http.cors.enabled: true
http.cors.allow-credentials: true
http.cors.allow-origin: "*"
http.cors.allow-headers: X-Requested-With, X-Auth-Token, Content-Type, Content-Length, Authorization, Access-Control-Allow-Headers, Accept
```

### 3. Data Visualizer経由でCSVをimportする

Kibana 6.5から[Data Visualizer経由で100MBまでのCSVであればElasticsearchにデータをインポート](https://www.elastic.co/guide/en/kibana/6.5/release-highlights-6.5.0.html#_visualize_data_from_a_log_file)できるようになりました。
中々便利なので、今回はこれでインポートしてみます。6.4以前を使っている場合は[Logstash](https://www.elastic.co/jp/products/logstash)など他のツールでCSVをインポートしてください。

メニューのMachine Learning -> Data Visualizerをクリックします。

<img width="1747" alt="data-visualizer-for-file-input.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264679/50e8f0db-dc9e-cf2e-3756-38f971168a89.png">

画面中央に`games-features.csv`をドラッグアンドドロップします。

<img width="1733" alt="data-visualizer-for-file-analysis.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264679/0f5076ca-05fc-1c2c-0f29-679fbf035387.png">

すると、データを解析して、CSVであることを解釈し、それぞれのカラムについてのデータ傾向を表示してくれます。
インポート目的ではなくてデータの傾向を把握したい時なんかにも便利です。

軽く眺めて満足したら、左下の"Import"をクリックします。
インポート設定画面に飛んだら、今回はingest pipelineを実行したい部分があるので、"Advanced"をクリックします。

<img width="1744" alt="data-visualizer-for-file-ingest-pipeline.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264679/a5787249-6f8e-65cd-661c-1fa5b4adfa6d.png">

実は、今回のデータセットでは一部ゲームデータが重複しています。
したがって、ゲームのIDである`ResponseID`というフィールドをドキュメントidに設定するingest pipelineを実行することで、重複をなくします。

画面右側の`ingest pipeline`に以下のコードを入力します。


```
{
  "processors" : [
    {
      "set" : {
        "field" : "_id",
        "value" : "{{ResponseID}}"
      }
    }
  ]
}
```

その後、"Import"をクリックします。

<img width="1733" alt="data-visualizer-for-file-result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264679/aa68c8fd-8b7f-a2b2-a3f8-85f5bf985bd6.png">

無事インポートされました。
そのままDiscoverなどでも閲覧できます。

<img width="1734" alt="kibana-discover.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264679/0838b1b6-fc9d-732d-d2df-7d9dc1394b09.png">


これでデータ側は準備完了です。

## React.jsアプリの雛形を作って起動する(5分)

次に、React.jsアプリを作る前準備を実行します。
ほぼreact-create-appの実行とreactivesearchをインストールする時間です。

### 前提条件
以下のツールがインストール済みの環境であること
※参考までに動作確認時のバージョンを記載してますが、違うバージョンでも普通に動くと思います。

- Elasticsearch 7.4.0
- Kibana 7.4.0
- Node.js v12.11.1
- npm 6.11.3

### 1. create-react-appしてReactアプリの雛形を生成する
実行が終わるまで気長に待ちます。


```bash
npx create-react-app steam-search
```

### 2. Reactivesearchをインストールする
こっちも依存関係が多くて時間がかかりますが、気長に待ちます。
なお、本記事執筆時点での`@appbaseio/reactivesearch@2.14.1`では`ws`のバージョンが古いため、`npm audit`で警告が出ます。このバージョンをプロダクションで使うのは控えた方が良いかもしれません。


```bash
cd steam-search
npm install @appbaseio/reactivesearch
```

3. Reactアプリを起動する


```bash
npm start
```

4. ブラウザで http://localhost:3000 にアクセスしてReactアプリの初期画面が表示されることを確認する

<img width="748" alt="react.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264679/2ff32fed-4c56-7ca8-8622-de0a91fd55b6.png">

これでアプリ側も準備完了です。
以後、React.jsアプリは起動したまま、修正を加えていきます。


## Reactivesearchでシンプルな検索GUIを作ってみる(5分)

ではまず、ゲームのタイトルを全文検索して、結果にゲームタイトルを表示する簡易的な検索UIを作ってみます。

### 1. 以下のコードを`src/App.js`にコピペする
元々の`src/App.js`のコードは全部削除して上書き保存します。


```jsx
import React, {Component} from 'react';
import {DataSearch, ReactiveBase, ReactiveList, ResultList, SelectedFilters} from '@appbaseio/reactivesearch';
import './App.css';

const { ResultListWrapper } = ReactiveList;

class App extends Component {
    render() {
        return (
            <div className="main-container">
                <ReactiveBase
                    app="steam-search"
                    url="http://localhost:9200"
                    credentials="elastic:changeme"
                >
                    <DataSearch
                        componentId="title"
                        dataField={["ResponseName"]}
                        queryFormat="and"
                    />
                    <SelectedFilters/>
                    <ReactiveList
                        componentId="resultLists"
                        dataField="ResponseName"
                        size={10}
                        pagination={true}
                        react={{
                            "and": ["title"]
                        }}
                    >
                        {({data}) => (
                            <ResultListWrapper>
                                {
                                    data.map(item => (
                                        <ResultList key={item._id}>
                                            <ResultList.Content>
                                                <ResultList.Title
                                                    dangerouslySetInnerHTML={{
                                                        __html: item.ResponseName
                                                    }}
                                                />
                                            </ResultList.Content>
                                        </ResultList>
                                    ))
                                }
                            </ResultListWrapper>
                        )}
                    </ReactiveList>
                </ReactiveBase>
            </div>
        );
    }
}

export default App;
```

すると、画面上 (http://localhost:3000) の表示は以下のように変化します。

![Screenshot_2018-12-15 React App(4).png](https://qiita-image-store.s3.amazonaws.com/0/264679/40e01887-9005-cb8c-f777-39397254b697.png)

もし画面を開いた瞬間に結果が"No Results found."と表示される場合は以下の可能性が考えられます。環境を見直してください。

- Elasticsearchが起動していない
- Elasticsearchに今回のデータセットが"steam-search"というインデックス名で投入できていない
- CORS回避の設定をElasticsearchに設定せず起動してしまった

### 2.挙動の確認

"Search"部分に文字を入力し始めるとゲームタイトルのオートコンプリートがサジェストされます。

![Screenshot_2018-12-15 React App(3).png](https://qiita-image-store.s3.amazonaws.com/0/264679/72be9f5e-19ae-bb85-9f0b-4164a9ebfbbf.png)

また、"Search"部分で文字を入力してEnterキーを押下するか、オートコンプリートのサジェストを選択すると、該当の文字列が全文検索の条件になり、検索結果が変更されます。

![Screenshot_2018-12-15 React App(2).png](https://qiita-image-store.s3.amazonaws.com/0/264679/31c0f9c9-1d98-07b6-b4b3-3966e4dcd51f.png)

画面上部の"title: fez"と表示されている検索条件や"Clear All"と表示されているバッジをクリックすると、その条件を削除できます。

また、スペース区切りで文字を入力すると、AND条件で検索されます。

![Screenshot_2018-12-15 React App(6).png](https://qiita-image-store.s3.amazonaws.com/0/264679/37b83493-855d-8b22-4494-23cb0801163a.png)

これで見た目はさておき、以下を実現できました。

- ゲームタイトルに対する全文検索
  - 文字を入力し始めるとゲームタイトルのオートコンプリートがサジェストされる
  - スペース区切りで入力するとAND条件で検索される
- 設定されている検索条件の表示
- 検索結果としてゲームタイトルを表示する

### 2. コードの解説

Reactivesearchは[ReactiveBase](https://opensource.appbase.io/reactive-manual/getting-started/reactivebase.html)というコンポーネントを最上位コンポーネントとして持ち、その中に検索条件入力用のコンポーネントや結果表示用のコンポーネントを配置していきます。イメージとしては以下の形に必ずなります。

```jsx
{/* 必ず最上位コンポーネントとしてReactiveBaseを配置する */}
<ReactiveBase>

  {/* 検索条件入力用のコンポーネント */}
  <HogeSearch />
  
  {/* 検索結果表示用のコンポーネントはReactiveListの下に配置する */}
  <ReactiveList>
    <ResultHoge/>
  </ReactiveList>
  
</ReactiveBase>
```

イメージコードでは適当なコンポーネント名にしていますが、それぞれ使用できるコンポーネントは様々な種類が用意されています。詳しくは[ドキュメント](https://opensource.appbase.io/reactive-manual/base-components/textfield.html)を参照してください。

今回のコードではReactiveBaseの内部で3種類のコンポーネントを使用しています。それぞれの役割を以下に示します。

- [DataSearch](https://opensource.appbase.io/reactive-manual/search-components/datasearch.html)
  - オートコンプリート付きの全文検索フォームの提供
- [SelectedFilters](https://opensource.appbase.io/reactive-manual/base-components/selectedfilters.html)
  - 設定されている検索条件の表示と解除
- [ResultList](https://opensource.appbase.io/reactive-manual/result-components/resultlist.html)
  - 検索結果の表示

画面上で言うと以下のイメージです。

![Screenshot_2018-12-15 React App_mod.png](https://qiita-image-store.s3.amazonaws.com/0/264679/8fc06f20-bbb5-fb48-2dbe-cb725867fb30.png)

この3種類のコンポーネントに幾つかのプロパティ(Props)を設定するだけで、良い感じに検索できるようになります。
それぞれのコンポーネントで設定しているPropsについて説明します。

**ReactiveBase**

| Props | 今回の値 | 説明 | 備考 |
|:-:|:-:|:-:|:-:|
| url | "http://localhost:9200" | ElasticsearchのHTTPホスト。 |-|
| app | "steam-search" | 検索対象のインデックス名。 |-|

**DataSearch**

| Props | 今回の値 | 説明 | 備考 |
|:-:|:-:|:-:|:-:|
| componentId | "title" | コンポーネントに付与する一意なID。 |-|
| dataField | {\["ResponseName"\]} | 検索対象のフィールド名。 |複数指定可。|
| queryFormat | "and" | スペース区切りで入力された時にOR条件にするかAND条件にするか。 |設定しない場合の初期値は"or"。|

**ResultList(ReactiveList)**

| Props | 今回の値 | 説明 | 備考 |
|:-:|:-:|:-:|:-:|
| componentId | "resultLists" | コンポーネントに付与する一意なID。 |-|
| pagination | {true} | 結果をページネーションで表示するかどうか。 |設定しない場合の初期値は{false}で、画面最下部までスクロールすると随時下に結果が追加される表示になる。 |
| size | {25} | ページネーションで1ページに表示する件数。 |-|
| react | {{ "and": \["title"\] }} | どの検索条件に反応して結果を表示するか。 | 該当の検索条件入力用コンポーネントで設定した"componentId"を指定する。|

なんとなく、Reactivesearchで最低限の実装を実現する方法が理解できたでしょうか？

## 機能を追加していく(5分)

次に、基本的な機能に加えて以下を追加していきます。

1. 結果表示にゲーム画像と発売時期、価格を表示し、クリックしたらゲームストアへリンクするようにする
1. 発売価格によるソート機能

### 1. 結果表示にゲーム画像と発売時期、価格を表示し、クリックしたらゲームストアへリンクするようにする

今回のデータセットにはゲームに関連した以下の情報が含まれています。

| フィールド名 | 説明 | 例 |
|:-:|:-:|:-:|
| HeaderImage | ヘッダー画像のURL。 | http://cdn.akamai.steamstatic.com/steam/apps/224760/header.jpg?t=1472521163 |
| ReleaseDate | 発売日。 | May 1 2013 |
| PriceInitial | 発売価格。単位はUSドル。 | 9.99 |
| ResponseID | Steamにおける該当ゲームのID。ストアでのゲームのURLに含まれる。 | 224760 |

これらのフィールドを使って、検索結果の表示を少しだけリッチにしてみます。

`ResultList`を以下のように変更します。

```diff
      <ResultList
          key={item._id}
+         href={`https://store.steampowered.com/app/${item.ResponseID}`}
      >
+         <ResultList.Image src={item.HeaderImage}/>
          <ResultList.Content>
              <ResultList.Title
                  dangerouslySetInnerHTML={{
                      __html: item.ResponseName
                  }}
              />
+             <ResultList.Description>
+                 <p className="releaseDate">${item.ReleaseDate}</p>
+                 <p className="price">$${item.PriceInitial}</p>
+             </ResultList.Description>
          </ResultList.Content>
      </ResultList>
```

画面を確認してみます。

![Screenshot_2018-12-15 React App(8).png](https://qiita-image-store.s3.amazonaws.com/0/264679/7b42ee22-f495-b379-f270-7771a9fe471c.png)


ゲームタイトルに加えて、先ほどの情報を追加表示できるようになりました。
また、結果をクリックすることで、Steamの該当ゲームのストアページへ飛ぶことが可能になりました。

### 2. 発売価格によるソート機能

`ReactiveList`のPropsとして`sortOptions`を追加します。

```diff
     <ReactiveList
         componentId="resultLists"
         dataField="ResponseName"
         size={10}
         pagination={true}
         react={{
             "and": ["title"]
         }}
+        sortOptions={[
+            {label: "Best Match", dataField: "_score", sortBy: "desc"},
+            {label: "Lowest Price", dataField: "PriceInitial", sortBy: "asc"},
+            {label: "Highest Price", dataField: "PriceInitial", sortBy: "desc"},
+        ]}
     >
```

画面を確認します。

![Screenshot_2018-12-15 React App(9).png](https://qiita-image-store.s3.amazonaws.com/0/264679/484ab121-589a-9657-9c2e-f091a2c20dcd.png)

検索ワード入力欄の右下にソート用のセレクトボックスが追加されました。
`Best Match`はクエリ発行結果の`_score`の降順に並べる設定です。

![Screenshot_2018-12-15 React App(10).png](https://qiita-image-store.s3.amazonaws.com/0/264679/2eef4515-01ce-643d-63ca-84bd36645989.png)

`Lowest Price`は発売価格の昇順なので、検索結果の序盤には無料ゲームが並んでいます。

![Screenshot_2018-12-15 React App(11).png](https://qiita-image-store.s3.amazonaws.com/0/264679/7a050f82-1598-9ba9-9da4-60f244a3c54c.png)

`Highest Price`は発売価格の降順なので、検索結果の序盤には高価な制作者向けツールやバンドルなどが並んでいます。

## CSSで画面を調整する(5分)

ここまできたら後はCSSで見た目を調整してフィニッシュです。

### 1. App.jsの修正

まず`App.js`を以下のように変更します。


```diff
  import React, {Component} from 'react';
  import {DataSearch, ReactiveBase, ReactiveList, ResultList, SelectedFilters} from '@appbaseio/reactivesearch';
  import './App.css';
+ import './SteamSearch.css'

  const {ResultListWrapper} = ReactiveList;

  class App extends Component {
      render() {
          return (
              <div className="main-container">
                  <ReactiveBase
                      app="steam-search"
                      url="http://localhost:9200"
                      credentials="elastic:changeme"
+                     theme={
+                         {
+                             typography: {
+                                 fontFamily: 'Arial, Helvetica, sans-serif',
+                                 fontSize: '16px',
+                             },
+                             colors: {
+                                 titleColor: '#c7d5e0',
+                                 textColor: '#c7d5e0',
+                                 backgroundColor: '#212121',
+                                 primaryColor: '#2B475E',
+                             }
+                         }
+                     }
                  >
                      <DataSearch
                          componentId="title"
                          dataField={["ResponseName"]}
                          queryFormat="and"
+                         placeholder="enter search term"
+                         showIcon={false}
+                         title="Steam Search"
+                         className="data-search"
+                         innerClass={{
+                             input: 'input',
+                             list: 'list',
+                         }}
                      />
                      <SelectedFilters/>
                      <ReactiveList
                          componentId="resultLists"
                          dataField="ResponseName"
-                         size={10}
+                         size={25}
                          pagination={true}
                          react={{
                              "and": ["title"]
                          }}
                          sortOptions={[
                              {label: "Best Match", dataField: "_score", sortBy: "desc"},
                              {label: "Lowest Price", dataField: "PriceInitial", sortBy: "asc"},
                              {label: "Highest Price", dataField: "PriceInitial", sortBy: "desc"},
                          ]}
+                         className="result-list"
+                         innerClass={{
+                             resultsInfo: "resultsInfo",
+                             resultStats: "resultStats",
+                         }}
                      >
                          {({data}) => (
                              <ResultListWrapper>
                                  {
                                      data.map(item => (
                                          <ResultList
                                              key={item._id}
                                              href={`https://store.steampowered.com/app/${item.ResponseID}`}
+                                             className="listItem"
                                          >
-                                             <ResultList.Image src={item.HeaderImage}/>
+                                             <ResultList.Image className="image" src={item.HeaderImage}/>
                      (以下省略)
```

まず、`ReactiveBase`に`theme`を設定することで、[配下のコンポーネントの共通設定を変更します](https://opensource.appbase.io/reactive-manual/theming/themes.html)。

次に、ページネーションの表示数や検索フォームのプレースホルダを[Steamの検索ページ](https://store.steampowered.com/search/)に合わせたりします。

あとは、各コンポーネントに`innerClass`を渡すことで、[各コンポーネント内部のパーツにclass名を付けます](https://opensource.appbase.io/reactive-manual/theming/class.html)。これで、CSSセレクタで指定しやすくなります。どのパーツにclass名をつけることができるかは[各コンポーネントのドキュメントのStyles](https://opensource.appbase.io/reactive-manual/search-components/datasearch.html#styles)に記載されています。

### 2. CSSをあてる

次に`src/SteamSearch.css`を新たに作成します。

```css
@font-face {
    font-family: "IonIcons";
    src: url("//code.ionicframework.com/ionicons/2.0.1/fonts/ionicons.eot?v=2.0.1");
    src: url("//code.ionicframework.com/ionicons/2.0.1/fonts/ionicons.eot?v=2.0.1#iefix") format("embedded-opentype"), url("//code.ionicframework.com/ionicons/2.0.1/fonts/ionicons.ttf?v=2.0.1") format("truetype"), url("//code.ionicframework.com/ionicons/2.0.1/fonts/ionicons.woff?v=2.0.1") format("woff"), url("//code.ionicframework.com/ionicons/2.0.1/fonts/ionicons.svg?v=2.0.1#Ionicons") format("svg");
    font-weight: normal;
    font-style: normal
}

html {
    min-height: 100%;
}

body {
    background-color: #1B3C53;
    background-image: linear-gradient(315deg, #000000 0%, #1B3C53 74%);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.main-container {
    width: 616px;
    margin-top: 20px;
}

.data-search .input {
    color: white;
}

.data-search .list li {
    background-color: #aaa !important;
    color: #000;
    overflow: hidden;
}

.data-search .list li:target {
    background-color: #666 !important;
}

.resultsInfo {
    position: relative;
}

.resultsInfo:before {
    z-index: 1;
    position: absolute;
    right: 15px;
    top: 0;
    content: "\f123";
    font-family: "IonIcons";
    line-height: 43px;
    color: #7F878C;
    pointer-events: none;
}

.resultsInfo select {
    background-color: #18394D;
    color: #67c1f5;
    outline: 1px solid #000;
    background: transparent;
    background-image: none;
    padding-right: 50px;
}

.resultsInfo option {
    background-color: #417A9B;
    color: #fff;
}

.result-list .resultStats {
    color: #3b6e8c;
}

.result-list .listItem {
    background-color: #16202D;
    padding: 0;
    margin: 2px 0;
    border: 0;
    background: rgba(0, 0, 0, 0.4);
}

.result-list .listItem:hover {
    background: rgba(0, 0, 0, 0.8);
}

.result-list .image {
    width: 120px;
    height: 45px;
}

.result-list article, .result-list article div {
    display: flex;
    align-items: center;
}

.result-list article h2 {
    padding: 0;
}

.releaseDate {
    color: #4c6c8c;
    width: 150px;
}

.price {
    width: 50px;
}

input {
    background-color: #1C3345 !important;
    border: 1px solid #000 !important;
}
```

`body`や`input`に直で何かを当てたり`!important`をバリバリ使っていたり、かなりアレな感じですが見なかったことにして…ここまでの手順を経ると画面は以下のようになっているはずです。

![Screenshot_2018-12-15 React App(12).png](https://qiita-image-store.s3.amazonaws.com/0/264679/6e638db3-cb98-211c-0a3c-2d730b81bfdd.png)

実際に動かしてみるとこんな感じです。

![ezgif-1-1a81359e991b.gif](https://qiita-image-store.s3.amazonaws.com/0/264679/6b5402db-aa18-7391-d863-e838bc23a94d.gif)

中々それらしい検索画面になったのではないでしょうか。

## おわりに

Reactivesearch、いかがだったでしょうか。
個人的にはかなり気に入りました。Elasticsearchで検索UIを作るときの[Bootstrap](https://getbootstrap.com/)的存在として流行ってもおかしくないと思います。
まだまだ使えていない機能がたくさんあるので、ちょこちょこ使っていこうかと思います。

最後に読まれた方が気になりそうな仮想質問にカジュアルに答えるコーナーを設けて終わろうと思います。

### 仮想質問に対するQ&A

- Q. ライセンスを教えろください
    - A. [Apache License 2.0](https://github.com/appbaseio/reactivesearch/blob/dev/LICENSE)なので安心して使えまする
- Q. エンドユーザのブラウザから直接Elasticsearchに接続できる必要があったりします？その場合、プロダクションで使うにはセキュリティ的にどうかと思うんですが。
    - A. [プロキシサーバーを間に挟む](https://opensource.appbase.io/reactive-manual/getting-started/reactivebase.html#connect-to-elasticsearch)ことで直接接続を回避できます。[公式の実装例](https://github.com/appbaseio-apps/reactivesearch-proxy-server)もあるので、その辺りを参考にすればよいと思います。また、ElasticsearchのSecurity機能で、最小権限のユーザーを作成し、使用することを推奨します。
- Q. CSS当てないとまともに使えないんですか？
    - A. 機能的には問題なく動きます。見た目の好みの問題です。[Themes](https://opensource.appbase.io/reactive-manual/theming/themes.html)機能を使いこなせれば、開発中は耐えうる見た目にできるかもしれません。
- Q. クエリを自動で発行してくれるみたいだけど、自分でカスタマイズはできないんですか？
    - A. カスタムクエリを設定する方法があるっぽいです。[クエリを発行するコンポーネントのドキュメント](https://opensource.appbase.io/reactive-manual/search-components/datasearch.html)にあるように、`defaultQuery`や`customQuery`をPropsとして設定する模様。
- Q. もっと他の実装、デザイン例はないんですか？
    - A. 公式で[デモサイト](https://opensource.appbase.io/reactivesearch/demo)が幾つかあるので、それを覗いたりすると良いと思います。リポジトリでコードも公開されているので参考になります。[GitXplore](https://opensource.appbase.io/reactivesearch/demos/gitxplore/)とかすごい。

### 本当のおわりに

以上です。

明日の枠はまだ空いてるみたいです。小ネタでも何でも共有してみてはいかがでしょうか！ではでは〜。