Firebase Algolia Extensionをブログ検索に利用する

# 前提
既にウェブサイトにブログ機能があるがユーザー側、管理者側に検索機構を用意していなかったので無料枠に収まる想定でalgoliaを採用し、設定と実装を進めた。

既存のウェブサイトの構成：
- Angular 16 / Angular MaterialからMatTable/MatPaginator
- firebase hosting / firestoreのcollectionにてblog_posts, 220件

algoliaにアカウントを作成しExtensionの追加の指示通りに進めた。
（支払い情報の登録は無し）

# 詰まったところ
結論としてはGCP側の権限と、Aloglia側の両方の権限の設定がミスっていて、
blog_postsのFirebaseのドキュメントを追加、修正するとexecuteIndexOperationにエラーが出る。
またはblog_postsとの連携フィールドを修正して再構成をしてもexecuteFullIndexOperationのエラーが出る。
インデックスにデータが追加されない状況になっていた。

エラー内容：
```
Error when performing Algolia index
    at entryFromArgs (/workspace/node_modules/firebase-functions/lib/logger/index.js:130:19)
    at Object.error (/workspace/node_modules/firebase-functions/lib/logger/index.js:116:11)
    at Object.error (/workspace/lib/logs.js:38:33)
    at handleUpdateDocument (/workspace/lib/index.js:100:14)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /workspace/lib/index.js:128:13
```

# 解消
- Extensionのインストール時にGCPのサービスアカウントを自動生成してくれているが権限は微妙に不足していたのでGCP側のIAM設定で指示とおりに以下のロールを設定する

　サービスアカウント：
　 ext-firestore-algolia-search@{project_name}.iam.gserviceaccount.com

　ロール：
　 ・Cloud Datastoreユーザー（自動生成）
　 ・Secret Managerのシークレットアクセサー（追加で付与）
　 ・クラウドタスクへのデータ追加（追加で付与）
 　
- Algolia側でもindex自体も予め作っておく
もしかしたら権限が揃っていればエラーなく自動生成してくれたのかもしれないので本当に必要かは分からない

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168300/5a376906-1a58-c955-4128-32a0a573bcab.png)
 　
- Firebase Extension側でAlgolia側のAPIキーを指定していたが、そのAPIキーに紐づく権限が不足していたので、自前でAPIキーに権限を割り振ったものを生成して、指定しなおした
もともとはAlgoliaの初期アプリ内に自動生成されていたSearchの権限しかないAPIキーをFirebase Extensionのコンソールで指定していた。
加えてAPIキーを作ろうともダッシュボードに入れず、Algolia側で再ログインしてもGet Startedの画面から出られず、APIキーの画面に遷移できなかった。
結局、API作成のURL直打ちで遷移して、APIキー作成画面で権限は指示通りに指定して、それ以外は初期値のまま作成した。
https://dashboard.algolia.com/account/api-keys/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168300/c11de34c-b1fc-ddd7-35a0-409a67278db2.png)

APIキーを発行したらFirebase Extensionのコンソールで設定(SecretManagerの更新も同画面で実施可)して「拡張機能を再構築する」ボタンを押す
その後、CloudFunctionのエラーが解消した。


# Angularでの実装
InstantSearch.jsはリッチすぎるのでliteを使おうとしたがドキュメント不足で通常版を利用

npm i algoliaしてalogolia 5.18が入ったことを確認。

- 検索するところ
```typescript
import { Injectable } from '@angular/core';
import { Algoliasearch, algoliasearch } from 'algoliasearch';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root',
})
export class AlgoliaSearchService {
  private client: Algoliasearch;

  constructor() {
    this.client = algoliasearch(
      environment.algolia_app_id,
      environment.algolia_api_key
    );
  }

  async search({
    query,
    filters, //`datetime_to_publish <= ${Date.now()}`
    hitsPerPage = 5,
    page = 0,
  }: {
    query: string;
    filters?: string;
    hitsPerPage?: number;
    page?: number;
  }) {
    const searchParams = {
      query,
      page,
      hitsPerPage,
      facetFilters: ['deleted:false'],
      ...(filters && { filters }),
    };

    const hits = await this.client.searchSingleIndex({
      indexName: environment.algolia_blog_index,
      searchParams,
    });
    return hits;
  }
}
```

- facetFilterとfiltersは予めAlgolia側のコンソールで指定しておいた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168300/afae6a43-3449-412f-e59c-eceee57b2cd1.png)


・検索結果を画面コンポーネントに渡すところ
```typescript
  paginateByAlgolia(event: PageEvent) {
    this._loadingService.setLoading(true);

    this._algoliaSearchService
      .search({ query: this.searchText, page: event.pageIndex })
      .then((res: any) => {
        this.blogList.set(res.hits);
        this.pagination.length = res.nbHits;
        this.pagination.pageIndex = event.pageIndex;
        this.pagination.pageSize = res.hitsPerPage;
      })
      .catch(() => {
        this._notificationService.error('データの取得に失敗しました');
      })
      .finally(() => {
        this._loadingService.setLoading(false);
        this.backToTop();
      });
  }
```

# 参考
Alogoloa側に作るインデックス名をちゃんとFirebase Extensionの設定画面で指定しなさいよという話
https://github.com/algolia/firestore-algolia-search/issues/51