AlgoliaとGraphQLで検索とインデクシング

こちらはAlgoliaブログに掲載された GraphQL search and indexing with Algolia の翻訳です。著者のCharly Polyはフリーランスのフロントエンドアーキテクトで、以前はAlgoliaでエンジニアをしていました(パリで一緒に数日間のパブリック・スピーキングのトレーニングに参加しました)。


Web上で広く使われているGraphQLは開発者に多くのメリットをもたらしています。フロントエンドでは、ラウンドトリップを減らし、クライアント側でのキャッシングを改善することができ、バックエンド側では、sef-describingでschema-basedなAPIを生成することで、マイクロサービスの多様性をシンプルにします。こちらの記事では、GraphQLを使って検索機能を強化する方法をご説明します。

Algoliaはパフォーマンスの優れた合理的なデータアクセス方法を提供することや、素晴らしい開発者体験に価値を見出すことなど、GraphQLと同様のコアバリューがあります。それによって、AlgoliaとGraphQLを組み合わせることで、開発者はbest-in-classな検索体験を構築するための豊富なツールを手に入れることが可能です。

これらの2つの技術がどのように連携するのかを見ていくために、架空の会社である flight-tickets.io がオンラインで航空券の価格の比較を行うサービスを例にします。flight-tickets.ioのシステムでは、GraphQLを使ってAlgoliaのバックエンドのIndexingプロセスをより良くし、次にAlgoliaのInstantSearchとunifiedなfront-endプログラミング環境をインテグレートしていきます。

GraphQLを使ってAlgoliaにデータをインデクシング

この会社のAlgoliaインデックスは何十もの外部のプロバイダーから送られてくるチケット価格を公開します。GraphQLがなければ、全てのプロバイダーのデータをAlgoliaのインデックスに投入するために複雑なETLを実装する必要があり、データソースごとにワーカーが必要でした。

フローとしては以下のようになります:

ご覧のように、ここには3つのステップがあります: 複数のプロバイダーからデータを抽出し、変換して、Algoliaにプッシュする。データソースの数やAPIの種類(REST, gRPC, …)が増えていくにつれて、このプロセスの複雑さは増していき、新しいプロバイダーとのインテグレーションやエンジニアリングチームからのサポートが遅れてしまう原因となります。

ここには安定性の問題も存在します:

  • 各ワーカーはデータプロバイダごとに複数のAPIを呼び出していた
  • 各プロバイダはそれぞれ独自のプロトコルでデータを公開していた: REST, gRPC, もしくはプロプライエタリ
  • 各プロバイダごとにユニークなカスタムエラー管理

GraphQL-basedなインデクシング

GraphQLを使ったETLの再実装は以下のようになります:

ここで、Pullとトランスフォーメーションステージが1つのGraphQLプロセスで管理(もしくは統合)されていることになります。GraphQL Meshに依存した新しいGraphQL内部APIを構築することで、データプロバイダとETLの間に1つのデータレイヤーを追加したことになります。

基本的に、ETLは以下のように統合されたやり方でクエリされる複数のデータソースを公開している内部のGraphQL APIから、データの変更を抽出したりサブスクラブしたりします。

query getTicketsForProvider($provider: Provider!, $from: Date!, $to: Date!, $cursor: Cursor) {
  tickets(provider: $provider, from: $from, to: $to, cursor: $cursor) {
    edges {
      node {
        provider {
          id
          name
        }
        prices {
          ...PriceByLocationAndTime
        }
        # ...
      }
      pageInfo {
        cursor {
          before
          after
        }
      }
    }
  }
}

データは統合されていて、エラーのハンドリングも含まれます。以下は、dataerrorフィールドを含むよくあるGraphQLエラーのレスポンスボディです。

{
  "data": {
     “tickets”: []
  },
  "errors": [
    {
      "message": "[SuperFlight API]: unavailable",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "extensions": {
        "code": "TICKETS_GRAPHQL_API_SUPERFLIGHT_ERROR",
        "exception": {
          "stacktrace": [
            "......",
          ]
        }
      }
    }
  ]
}

最後に、ネットワークレイテンシーの最適化に加えて、GraphQLはプロバイダーのデータ取得側でもパフォーマンスの最適化しています。GraphQL APIの実装は高度に非同期で行われるように実装されていて、各データフィードは並列に処理されるため、パフォーマンスが向上します。

補足:

AlgoliaのShopify extensionでは、ShopifyのデータをAlgoliaのIndexに登録するのにGraphQL APIを活用しています。ShopifyのGraphQL APIは、今まで説明してきたようなアドバンテージをもたらすだけでなく、更に進んでいます:

・Shopify REST APIと比較して、より寛容で柔軟なrate-limiting

・高度なページネーションパターン

Shopify REST APIからShopify GraphQL APIに移行した後、Shopifyデータの平均Indexingスピードは28%、Indexingのピークのキャパシティは150%増加しました

GraphQLとInstantSearchによる検索

ということで、GraphQLによって統合がなされたわけですが、この会社はSEOやデータをエンリッチメントする理由から、Algoliaの検索UIをGraphQLをバックエンドにしたものに置き換えたいと考えています。Algoliaではいつもフロントエンド検索(すなわちAlgoliaのサーバーにクライアントから直接クエリを行うことで爆速体験を提供するということ)を推奨していますが、航空券の予約ようなシステムの場合、余計なラウンドトリップが発生するとしても、リアルタイムなデータ取得のためにバックエンドサーバーを使うことは賢明なことであると思います。SEOに関しては、バックエンドとフロントエンドを組み合わせることで、あなたの会社のGoogle SEOをエンハンスすることができるでしょう。

Algolia GraphQL検索

GraphQLの検索クエリを既存のパブリックなGraphQL APIで公開するのが最初のステップです。これを実現するため、バックエンドチームは、Algolia IndexからGraphQLの型を生成するalgolia-graphql-schemaというツールを使いました。

flightsSearchのAlgoliaインデックスには以下のようなオブジェクトが含まれています:

{
  "provider_id": 12312312,
  "flight_company_name": "Bryan'air",
  "inbound": "2021-09-13T16:39:22.396Z",
  "outbound": "2021-19-13T16:39:22.396Z",
  "price": 1140.00
  // ...
}

まずは以下のスクリプトを package.json に追加します:

// ...
"scripts": {
  "generate:graphql": "algolia-graphql-schema"
},
// …

そしてスクリプトを走らせます:

$ npm link
$ npm run generate:graphql
 
> algolia-graphql-demo-server@1.0.0 generate:graphql
> algolia-graphql-schema
 
Analyzing flightsSearch index (that might take a few seconds...)
flightsSearch.graphql created!

そうすると flightsSearch.graphql というファイルが生成されます:

type AlgoliaResultObject {
  provider_id: String!
  flight_company_name: String!
  inbound: Date!
  outbound: Date!
  price: Int!
  # ...
}
 
type SearchResultsEdge {
  cursor: Int!
  node: AlgoliaResultObject
}
 
type SearchResults {
  edges: [SearchResultsEdge!]!
  totalCount: Int!
}
 
input SearchInput {
  query: String
  similarQuery: String
  sumOrFiltersScores: Boolean
  filters: String
  page: Int
  hitsPerPage: Int
  offset: Int
  length: Int
  attributesToHighlight: [String]
  attributesToSnippet: [String]
  attributesToRetrieve: [String]
  # ...
}
 
type Query {
  # ...
  search_flights($input: SearchInput!, $after: String): [SearchResults!]!
}

生成された検索の型定義を既存のGraphQL定義に統合して、Algoliaの検索APIを呼び出すリゾルバを実装し、バックエンドチームはフロントエンドチームにAlgoliaのInstantSearchを使った新しい検索UIの構築にGoサインを出しました。

Algolia InstantSearch GraphQL インテグレーション

フロントエンドチームは search_flights($input: SearchInput!, $after: String)GraphQLクエリを使ってフライトチケットを検索できるようになりました。そして幸いなことに、AlgoliaはバックエンドでInstantSearchを使う方法も公開しているのでセットアップはすぐに終わりました。

フロントエンドチームはAlgoliaのInstantSearch用に独自のGraphQL検索クライアントを実装しました。

import { SearchClient } from 'instantsearch.js'
 
const query = `
  Search($input: SearchInput!, $after: String) {
    search_flights(input: $input, before: $after) {
      edges {
        cursor
        node {
          provider_id
          flight_company_name
          inbound
          outbound
          price
          # ...
 
        }
      }
      totalCount
    }
  }
`;
 
const HITS_PER_PAGE = 20
 
const transformResponse = ({ query, page }) => (response) =>
  response.data?.search_flights.edges.reduce(
    (acc, edge) => {
      acc.results[0].hits.push(edge.nodes);
      acc.results[0].nbHits += 1
    },
    {
      results: [
        {
            hits: [],
            page,
            nbHits: response.data?.search_flights.edges.length || 0,
            nbPages: response.data?.search_flights.totalCount / HITS_PER_PAGE,
            query: query || "",
            exhaustiveNbHits: false,
            hitsPerPage: HITS_PER_PAGE,
            processingTimeMS: 0,
            params: ""
          }
      ],
    }
  );
 
const searchClient: SearchClient = {
  searchForFacetValues: (() => {}) as any,
  search(requests) {
    const request = requests[0]
    return fetch("https://api.flightickets.io/graphql", {
      method: "post",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        query,
        variables: { input: { ...request } },
      }),
    }).then(transformResponse({ query: request.query, page: request.page }));
  },
};

注: 検索UIのコードはGraphQL APIを通じて単一のAlgoliaインデックスをターゲットにしています

そして、このカスタム hits ウィジェットは、以下のようにGraphQL検索APIから生成されたTypeScriptの検索結果の型の恩恵を受けます。

// TypeScript types generated from the GraphQL API using GraphQL code generator
import { AlgoliaResultObject } from './graphql/generated'
 
const structuredResults = connectHits(({ hits, widgetParams }) => {
  const results = hits as AlgoliaResultObject[];
  // results.flight_company_name is autocompleted by TypeScript generated types
 
  const { container } = widgetParams;
 
  if (
    results
  ) {
    // Render the result
    // ...
    return;
  }
 
  // Render no results
  // ...
});

これで準備は整いました。AlgoliaをGraphQL APIと同様にフロントエンドのInstantSearchの設定をインテグレートする必要があります:

  • API側では、JavaScriptクライアントでAlgoliaを呼び出す
  • InstantSearch側では、searchClienthitsウィジェットをInstantSearchに提供する

まとめ

AlgoliaとGraphQLを組み合わせることで、flight-tickets.ioは全体的なユーザーとDeveloper Experienceを向上させました。

まず、GraphQLを使って、より高速に、そして安定した、リッチなIndexingパイプラインを構築し、何十ものプロバイダーからのデータの取得を統合しました。

  • 独自の言語とエラー管理
  • GraphQLの並列アーキテクチャによる一部のパフォーマンス向上
  • 複雑さの軽減とDeveloper Experienceの向上

最終的にGraphQLを活用してバックエンドの検索を構築することで、フロントエンドチームはAlgoliaのInstantSearchを使って、検索をフロントエンドスタックにパーフェクトに統合しました。

このポストについて何かフィードバックがあれば @whereischarly までどうぞ。

コメント

タイトルとURLをコピーしました