Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
109
Help us understand the problem. What is going on with this article?
@tschy

イケてる全文検索サービス「Algolia」を触ってみよう

More than 1 year has passed since last update.

はじめに

この記事は、株式会社メディアドゥのアドベントカレンダー19日目の記事です。

みなさん、「Algolia」って知っていますか?

Algoliaなんて知らないよー、聞いたこともないよー、というそこのあなた。
そんなあなたのためにこの記事を書いたといっても過言ではありません。
拙い記事ではありますが、ぜひ読んでみてください。

Algoliaとは?

以下のような特徴をもつ検索APIサービスです。

  • https://www.algolia.com/
  • 全文検索サービスをSaaSとして提供
  • GUIから検索ロジックを柔軟に設定可能
  • レコード数/APIアクセス数による従量課金
  • 世界各地にデータセンターがあり、どのロケーションでも高速な検索が可能
  • 様々なプログラミング言語向けにAPIを提供
  • 様々なフロントエンドフレームワーク向けに便利なライブラリを提供

本記事で書くこと

すでに知ってる・使ったことがある方には眠たい内容ですが、「Algoliaを使った全文検索の実装の流れ」みたいなものを紹介します。

個人的に、全文検索の実装って大変そうなイメージがあったんですが、Algoliaを使えばラクに動くものを実装できるので、その流れとイケてる感をふんわり掴んでもらえたらと思います。

なお、今回はAngular(フロントエンド)から直接クエリを投げて、全文検索結果を画面に表示するものを想定しています。おまけで、Golang(バックエンド)からAPI経由でデータを入れる、検索する、ぐらいの例も紹介します。

1.Algoliaへサインアップ

まずはこちらからアカウントを作って、サインアップします。
結構しっかりしたフリープランがあるのでお試しで使うだけならこれで十分。

登録を進めていくと、以下のように各地のデータセンターとのレイテンシーを見ながら、場所を選択できる画面がでてくるんですが、色々比べながら見てるとおもしろいです。当然Japanが圧倒的に爆速なので、ここではJapanを選択しました。
スクリーンショット 2019-12-17 11.12.57.png

2.Indexの作成

Algoliaのダッシュボードへアクセスすると、Indexの作成を促されるのでそのまま作成します。
Indexはなんでも良いので好きなように作成してOKです。ここではtest_indexとしました。
スクリーンショット 2019-12-17 11.39.22.png

3.検索対象となるデータを入れる

AlgoliaにIndexを作ると、そのIndexに紐づけてレコード(実際に検索対象になるデータ)を追加できるようになります。レコードの追加方法は、以下のとおりで様々あります。

  • ダッシュボードからJSONデータを直接入力して追加する
  • ダッシュボードからJSONファイルをインポートして追加する
  • API経由でレコードを追加する

実際にシステムとして運用する場合はAPI経由一択だと思いますが、今回は一番手っ取り早い「ダッシュボードからJSONデータを直接入力して追加する」方法でデータを入れることにします。

追加したJSONデータは以下のとおり。objectIDというフィールドがありますが、これはAlgolia側で必要になるユニークなIDです。このフィールドを事前に持たせていないと、Algolia側で勝手にランダム生成されるため、追加するレコードが持つPrimaryKeyをobjectIDとしても持たせておくのが良さそうです。

input_json_data

[
    {
        "objectID": "1",
        "Name": "山田一郎",
        "FavoriteFoods": [
            "寿司",
            "すき焼き"
        ]
    },
    {
        "objectID": "2",
        "Name": "鈴木二郎",
        "FavoriteFoods": [
            "オムライス",
            "ビーフシチュー"
        ]
    },
    {
        "objectID": "3",
        "Name": "山本三郎",
        "FavoriteFoods": [
            "寿司",
            "ビーフカレー"
        ]
    }
]

ちなみに、レコードを入れた後のダッシュボードの画面はこんな感じ。
スクリーンショット 2019-12-17 12.40.43.png

4.検索可能な属性を設定

Algoliaでは、検索可能にする属性を決めてそれぞれに重み付けをすることで、全文検索を柔軟に設定することができます。これらの設定はGUIでもAPIでもできますが、今回はGUIから行います。

設定画面は以下のとおり。
スクリーンショット 2019-12-17 15.44.14.png

今回は、NameおよびFavoriteFoodsのどちらの属性でも検索できるようにしました。
また、どちらもorderedと設定しているため、上にある属性のほうが優先度が高くなります。

つまり、ある検索クエリに対してNameFavoriteFoodsのどちらもヒットすることがあった場合、より上位に表示されるのはName属性でヒットしたレコードということになります。

5.APIキーの確認

アプリケーションからAlgoliaのAPIを叩くときに必要なAPIキーですが、ダッシュボードから確認できます。下記の画像ではグレーで隠していますが、ここにAPIキーがあります。

この後のフロントエンド編で必要になるので、Application IDSearch-Only API Keyを控えておきます。

スクリーンショット 2019-12-17 15.58.59.png

6.フロントエンド

ここまでにAlgoliaの最低限の設定はひと通り終わったので、残すはフロントエンドの実装です。

冒頭のAlgoliaの特徴でも書きましたが、フロントエンドフレームワークのためのライブラリが充実しており、それらを使うととても簡単に検索UIが実装できるようになっています。

本記事では、Angularを使うことを前提としているため、 InstantSearch for Angular を使って実装を進めることとします。

スクリーンショット 2019-12-17 13.23.56.png

より詳細を知りたい方は、InstantSearchの公式ページをチェックしてみてください。

インストール

以下コマンドにて、AngularプロジェクトにInstantSearchをインストールします。

Terminal

$ npm install algoliasearch angular-instantsearch@beta instantsearch.js instantsearch.css
$ npm install --save-dev @types/algoliasearch

NgAisModuleをインポート

Angularプロジェクトのルートモジュールに以下を追加してインポートします。
これでアプリ内でInstantSearchが使えるようになりました。

app.module.ts

import { NgAisModule } from 'angular-instantsearch'; //この行を追加

@NgModule({
  imports: [
    ******,
    ******,
    NgAisModule //この行を追加
  ]

polyfills.tsの更新

polyfills.tsの下部へ以下のとおりに追記を行います。

polyfills.ts
(window as any).process = {
  env: { DEBUG: undefined },
};

config設定

見てのとおりなのですが、AlgoliaのAPIキーやIndex名を与えてconfigオブジェクトを生成します。
Application IDSearch-Only API Keyは各々が控えたものを入れてください。

app.component.ts
import { Component } from '@angular/core';
import * as algoliasearch from 'algoliasearch/lite';

const searchClient = algoliasearch(
  '##########', // Application IDを入れる
  '################################' // Search-Only API Keyを入れる
);

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'algolia-search';

  config = {
    indexName: 'test_index', // Index名を入れる
    searchClient
  };
}

検索UIの実装

最後に、InstantSearchへconfigオブジェクトを渡して、検索ボックス(ais-search-box)および検索結果(ais-hits)のためのディレクティブを呼び出すだけでOKです。とりあえず全文検索が動いていることがわかればいいや、ぐらいで以下のようにしました。

app.component.html
<h2>Angular InstantSearch demo</h2>
<ais-instantsearch [config]="config">
  <ais-search-box></ais-search-box>
  <ais-hits>
    <ng-template let-hits="hits">
      <ol class="ais-Hits-list">
        <li *ngFor="let hit of hits" class="ais-Hits-item">
          <div>氏名:{{hit.Name}}</div>
          <div>好きな食べ物:{{hit.FavoriteFoods}}</div>
          <div>---------------------------------</div>
        </li>
      </ol>
    </ng-template>
  </ais-hits>
</ais-instantsearch>

できたもの

今までの工程を経て、でできたものを動かしたgif動画がこちらになります。
レコード数や属性が少なすぎる点は置いといて、一応全文検索されていることは分かるかなーと思います。

AlgoliaSearch.gif

よく見ると、何も入力されていないと検索候補が表示されてしまっていますが、Algoliaのデフォルトの動きがこうなっています。もちろんきちんと実装すればこの辺はいい感じにできます。

紹介できなかった便利機能

InstantSearchを使うと、とても簡単に検索UIの実装ができるということはわかってもらえたかと思いますが、さらにより便利に使えそうな機能があったのでそれらの一部も紹介します。

おまけ1. Go API経由で検索対象データを入れる

ここからおまけです。
まずは、検索対象データをAlgoliaへ入れるときは以下の感じでいけます。
バッチ処理などで複数レコードを入れるときはレコード数だけループを回すより、一括で以下みたいにオブジェクトにまとめて入れたほうが効率よいみたいです。
https://www.algolia.com/doc/api-reference/api-methods/save-objects/

main.go
package main

import (
    "github.com/algolia/algoliasearch-client-go/v3/algolia/search"
)

type Person struct {
    ObjectID      string   `json:"objectID"`
    Name          string   `json:"Name"`
    FavoriteFoods []string `json:"FavoriteFoods"`
}

func main() {
    persons := []Person{
        {
            ObjectID: "1",
            Name:     "山田一郎",
            FavoriteFoods: []string{
                "寿司",
                "すき焼き",
            },
        },
        {
            ObjectID: "2",
            Name:     "鈴木二郎",
            FavoriteFoods: []string{
                "オムライス",
                "ビーフシチュー",
            },
        },
        {
            ObjectID: "3",
            Name:     "山本三郎",
            FavoriteFoods: []string{
                "寿司",
                "ビーフカレー",
            },
        },
    }

    applicationID := "ALGOLIA_APPLICATION_ID" //Application IDを入れる
    adminAPIKey := "ALGOLIA_ADMIN_API_KEY" //Admin API Keyを入れる
    indexName := "test_index"

    client := search.NewClient(applicationID, adminAPIKey)
    index := client.InitIndex(indexName)

    _, err := index.SaveObjects(persons)
    if err != nil {
        panic(err)
    }
}

おまけ2. Go API経由で検索結果を取得する

次にAlgoliaに検索をかけて、結果を取得する場合は以下の感じです。
シンプルに検索するだけであればSearchメソッドにクエリキーワードをいれるだけでOKです。
返り値には、ヒットしたレコードのObjectIDだったり、matchLevelだったり、ヒットした部分だけが強調された文字列などが返ってきます。
https://www.algolia.com/doc/api-reference/api-methods/search/

main.go
package main

import (
    "fmt"

    "github.com/algolia/algoliasearch-client-go/v3/algolia/search"
)

func main() {
    applicationID := "ALGOLIA_APPLICATION_ID" //Application IDを入れる
    adminAPIKey := "ALGOLIA_ADMIN_API_KEY" //Admin API Keyを入れる
    indexName := "test_index"

    client := search.NewClient(applicationID, adminAPIKey)
    index := client.InitIndex(indexName)


    res1, err1 := index.Search("ビーフ")
    if err1 != nil {
        panic(err1)
    }
    fmt.Println("検索ワード: ビーフ")
    fmt.Println("マッチしたレコード数:", res1.NbHits)
    fmt.Println("マッチした内容:", res1.Hits)


    res2, err2 := index.Search("山田")
    if err2 != nil {
        panic(err2)
    }
    fmt.Println("検索ワード: 山田")
    fmt.Println("マッチしたレコード数:", res2.NbHits)
    fmt.Println("マッチした内容:", res2.Hits)
}
実行結果
$ go run main.go
検索ワード: ビーフ
マッチしたレコード数: 2
マッチした内容: [map[FavoriteFoods:[寿司 ビーフカレー] Name:山本三郎 _highlightResult:map[FavoriteFoods:[map[matchLevel:none matchedWords:[] value:寿司] map[fullyHighlighted:false matchLevel:full matchedWords:[ビ ー フ] value:<em>ビーフ</em>カレー]] Name:map[matchLevel:none matchedWords:[] value:山本三郎]] objectID:3] map[FavoriteFoods:[オムライス ビーフシチュー] Name:鈴木二郎 _highlightResult:map[FavoriteFoods:[map[matchLevel:none matchedWords:[] value:オムライス] map[fullyHighlighted:false matchLevel:full matchedWords:[ビ ー フ] value:<em>ビーフ</em>シチュー]] Name:map[matchLevel:none matchedWords:[] value:鈴木二郎]] objectID:2]]

検索ワード: 山田
マッチしたレコード数: 1
マッチした内容: [map[FavoriteFoods:[寿司 すき焼き] Name:山田一郎 _highlightResult:map[FavoriteFoods:[map[matchLevel:none matchedWords:[] value:寿司] map[matchLevel:none matchedWords:[] value:すき焼き]] Name:map[fullyHighlighted:false matchLevel:full matchedWords:[山 田] value:<em>山田</em>一郎]] objectID:1]]

終わりに

Algoliaの回し者みたいな記事になりましたが、サクッと使えてとても便利だなーと個人的には思ってます。もし興味をもっていただけたならぜひ触ってみてください!

参考

Algolia公式ドキュメント

109
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
tschy
mediado
私たちメディアドゥは、電子書籍を読者に届けるために「テクノロジー」で「出版社」と「電子書店」を繋ぎ、その先にいる作家と読者を繋げる「電子書籍取次」事業を展開しております。業界最多のコンテンツラインナップとともに最新のテクノロジーを駆使した各種ソリューションを出版社や電子書店に提供し、グローバル且つマルチコンテンツ配信プラットフォームを目指しています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
109
Help us understand the problem. What is going on with this article?