Amazon Web Services ブログ

Apache Solr から OpenSearch への移行

OpenSearch は、オープンソースの分散型検索エンジンで、e コマース検索、エンタープライズ検索 (コンテンツ管理検索、ドキュメント検索、ナレッジ管理検索など)、サイト検索、アプリケーション検索、セマンティック検索など、幅広いユースケースに適しています。また、インタラクティブなログ分析、リアルタイムアプリケーションモニタリング、セキュリティ分析などを実行できる分析スイートでもあります。Apache Solr と同様に、OpenSearch はドキュメントセットに対する検索を提供します。OpenSearch にはデータの取り込みと分析を行う機能も含まれています。Amazon OpenSearch Service は、AWS クラウドで OpenSearch のデプロイ、スケーリング、モニタリングを行うことができるフルマネージドサービスです。

多くの組織が Apache Solr ベースの検索ソリューションを OpenSearch に移行しています。主な要因は、総所有コストの低減、スケーラビリティ、安定性、発展した取り込みコネクタ (Data Prepper, Fluent Bit, OpenSearch Ingestion など)、Zookeeper のような外部クラスタマネージャーの排除、レポーティングの強化、OpenSearch Dashboards によるリッチな可視化などです。

Solr から OpenSearch への移行は、検索ソリューションを OpenSearch 向けに最適化するために完全なリファクタリングを行うアプローチをお勧めします。Solr と OpenSearch はどちらも Apache Lucene をコアのインデックス作成とクエリ処理に使用していますが、システムの特性は異なります。計画を立て、実証実験を行うことで、OpenSearch から最良の結果を得ることができます。このブログ記事では、Solr から OpenSearch への移行に関わる戦略的考慮事項と手順について詳しく説明します。

主な違い

Solr と OpenSearch Service は、Apache Lucene を通じて提供される基本的な機能を共有しています。しかし、用語や機能には以下のような主な違いがあります。

  • コレクションとインデックス: OpenSearch では、コレクションはインデックスと呼ばれます。
  • シャードとレプリカ: Solr と OpenSearch の両方で、シャードとレプリカという用語が使用されています。
  • API 駆動のインタラクション: OpenSearch ではすべてのインタラクションが API 駆動であり、手動でのファイル変更や Zookeeper の設定が不要です。OpenSearch インデックスを作成する際、マッピング (スキーマに相当) と設定 (solrconfig に相当) をインデックス作成 API 呼び出しの一部として定義します。

基本的な事項を説明したところで、4 つの主要コンポーネントについて、 Solr から OpenSearch に移行する方法を詳しく見ていきましょう。

コレクションからインデックスへ

Solr のコレクションは、OpenSearch ではインデックスと呼ばれます。Solr のコレクションと同様に、OpenSearch のインデックスにもシャードとレプリカがあります。

シャードとレプリカの概念は両方の検索エンジンで類似していますが、この移行を機に、より良いシャーディング戦略を採用することができます。シャード戦略のベストプラクティスに従って、OpenSearch のシャード、レプリカ、インデックスのサイズを決定してください。

移行の一環として、データモデルを再考してください。データモデルを検討することで、検索のレイテンシーとスループットを大幅に改善する可能性を見出すことができます。データモデリングが不適切だと、検索パフォーマンスの問題だけでなく、他の領域にも影響が及びます。例えば、特定の機能を実装するための効果的なクエリの実装が難しくなる場合があります。このような場合、解決策としてデータモデルの修正が求められる可能性があります。

違い: Solr では、プライマリシャードとレプリカシャードを同じノードに配置できます。OpenSearch では、プライマリとレプリカは同じノードに配置できません。さらに、OpenSearch Service は ゾーンアウェアネスにより、シャードを自動的に異なる Availability Zone (データセンター) に分散させることで、回復力を高めることが可能です。

OpenSearch と Solr のレプリカの概念は異なります。OpenSearch では、number_of_shards を使用してプライマリシャード数を定義し、データのパーティショニングを決定します。その後、number_of_replicas を使用してレプリカ数を設定します。各レプリカはすべてのプライマリシャードのコピーです。つまり、number_of_shards を 5 に設定し、number_of_replicas を 1 に設定すると、10 個のシャード (5 個のプライマリシャードと 5 個のレプリカシャード) が生成されます。Solr で replicationFactor=1 を設定すると、データの 1 つのコピー (単一のプライマリのみ) が生成されます。

例えば、以下は Solr に対して、 test という名前のコレクションを 1 つのシャードと 0 個のレプリカで作成しています。

http://localhost:8983/solr/admin/collections?
  _=action=CREATE
  &maxShardsPerNode=2
  &name=test
  &numShards=1
  &replicationFactor=1
  &wt=json

以下は、OpenSearch に対して、 test という名前のインデックスを 5 つのシャードと 1 つのレプリカで作成しています。

PUT test
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1
  }
}

スキーマからマッピングへ

Solr の schema.xml または managed-schema には、フィールド定義、動的フィールド、コピーフィールドのすべてが、フィールドタイプ (テキストアナライザー、トークナイザー、またはフィルター) とともに含まれています。スキーマの管理には schema API を使用します。または、スキーマレスモードで実行することもできます。

OpenSearch には動的マッピングがあり、Solr のスキーマレスモードのように動作します。データを取り込むために事前にインデックスを作成する必要はありません。新しいインデックス名でデータをインデクシングすることで、OpenSearch マネージドサービスのデフォルト設定 (例: “number_of_shards”: 5, “number_of_replicas”: 1) と、インデクシングされたデータに基づくマッピング (動的マッピング) でインデックスが作成されます。

しかしながら、事前に定義された厳密なマッピングを選択することを強くお勧めします。OpenSearch は、フィールドで最初に見た値に基づいてスキーマを設定します。実際には文字列フィールドであるものに、数値が最初の値として現れると、OpenSearch は不適切にそのフィールドを数値 (例えば整数) としてマッピングします。その後、そのフィールドに文字列値でインデクシングを試みると、不適切なマッピング例外で失敗します。データの内容とフィールドタイプを把握しているのであれば、マッピングを直接設定することにメリットがあります。

ヒント: サンプルデータを用いてインデックス作成を行い、初期マッピングを生成してください。その後、マッピングを修正・整理して実際のインデックスを正確に定義することを検討してください。このアプローチは、マッピングを最初から手動で構築するのを避けるのに役立ちます。

Observability ワークロードの場合は、Simple Schema for Observability の使用を検討してください。Simple Schema for Observability (ss4o としても知られる) は、共通の統一された observability スキーマに準拠するための標準です。このスキーマを導入することで、Observability ツールはデータを取り込み、自動的に抽出し、集約してカスタムダッシュボードを作成し、システムをより高いレベルで理解しやすくします。

多くのフィールドタイプ (データタイプ)、トークナイザー、フィルターは Solr と OpenSearch の両方で同じです。どちらも Lucene の Java 検索ライブラリをコアとして使用しているからです。

例を見てみましょう。

<!-- Solr schema.xml スニペット -->
<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" /> 
<field name="name" type="string" indexed="true" stored="true" multiValued="true"/>
<field name="address" type="text_general" indexed="true" stored="true"/>
<field name="user_token" type="string" indexed="false" stored="true"/>
<field name="age" type="pint" indexed="true" stored="true"/>
<field name="last_modified" type="pdate" indexed="true" stored="true"/>
<field name="city" type="text_general" indexed="true" stored="true"/>

<uniqueKey>id</uniqueKey>

<copyField source="name" dest="text"/>
<copyField source="address" dest="text"/>

<fieldType name="string" class="solr.StrField" sortMissingLast="true" />
<fieldType name="pint" class="solr.IntPointField" docValues="true"/>
<fieldType name="pdate" class="solr.DatePointField" docValues="true"/>

<fieldType name="text_general" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
    <tokenizer class="solr.StandardTokenizerFactory"/>
    <filter class="solr.ASCIIFoldingFilterFactory" preserveOriginal="false" />
    <filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
    <tokenizer class="solr.StandardTokenizerFactory"/>
    <filter class="solr.ASCIIFoldingFilterFactory" preserveOriginal="false" />
    <filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
PUT index_from_solr
{
  "settings": {
    "analysis": {
      "analyzer": {
        "text_general": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "asciifolding"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword",
        "copy_to": "text"
      },
      "address": {
        "type": "text",
        "analyzer": "text_general"
      },
      "user_token": {
        "type": "keyword",
        "index": false
      },
      "age": {
        "type": "integer"
      },
      "last_modified": {
        "type": "date"
      },
      "city": {
        "type": "text",
        "analyzer": "text_general"
      },
      "text": {
        "type": "text",
        "analyzer": "text_general"
      }
    }
  }
}

Solr と比較して OpenSearch で注目すべき点は以下の通りです。

  • _id は常に一意です。また、明示的に定義する必要はありません。指定されない場合は自動で採番されるためです。
  • マルチバリューを明示的に有効にする必要はありません。OpenSearch は、各フィールドに 0 個以上の値を含むことができます。
  • マッピングとアナライザーはインデックス作成時に定義されます。新しいフィールドを追加したり、特定のマッピングパラメータを後で更新したりすることはできますが、フィールドを削除することはできません。ReIndex API でこの問題を解決できます。Reindex API を使用して、あるインデックスから別のインデックスにデータをインデクシング可能です。
  • デフォルトでは、アナライザーはインデックス時とクエリ時の両方に適用されます。一部のケースでは、クエリ時に異なるアナライザーを指定することができます。これにより、インデックスマッピングで定義されたアナライザーがオーバーライドされます。
  • インデックステンプレートも、事前定義されたマッピングと設定で新しいインデックスを初期化する優れた方法です。例えば、ログデータ といった任意の時系列データを継続的にインデクシングする場合、インデックステンプレートを定義して、すべてのインデックスが同じ数のシャードとレプリカを持つようにすることができます。また、動的マッピング制御やコンポーネントテンプレートにも使用できます。

検索ソリューションを最適化する機会を探してください。例えば、分析の結果、city フィールドが検索ではなくフィルタリングにのみ使用されていることが判明した場合、不必要なテキスト処理を削減するために、そのフィールドタイプを text から keyword に変更することを検討してください。もう一つの最適化として、user_token フィールドが表示目的でのみ使用される場合、doc_values を無効にすることが考えられます。doc_values は text データタイプではデフォルトで無効になっています。

SolrConfig から OpenSearch の設定へ

Solr では、solrconfig.xml がコレクション設定を担っています。インデックスの場所、フォーマット、キャッシング、コーデックファクトリ、サーキットブレーカー、コミット、トランザクションログから、スロークエリ設定、リクエストハンドラ、update processing chain に至るまで、あらゆる種類の設定が含まれています。

例を見てみましょう。

<codecFactory class="solr.SchemaCodecFactory">
<str name="compressionMode">`BEST_COMPRESSION`</str>
</codecFactory>

<autoCommit>
    <maxTime>${solr.autoCommit.maxTime:15000}</maxTime>
    <openSearcher>false</openSearcher>
</autoCommit>

<autoSoftCommit>
    <maxTime>${solr.autoSoftCommit.maxTime:-1}</maxTime>
    </autoSoftCommit>

<slowQueryThresholdMillis>1000</slowQueryThresholdMillis>

<maxBooleanClauses>${solr.max.booleanClauses:2048}</maxBooleanClauses>

<requestHandler name="/query" class="solr.SearchHandler">
    <lst name="defaults">
    <str name="echoParams">explicit</str>
    <str name="wt">json</str>
    <str name="indent">true</str>
    <str name="df">text</str>
    </lst>
</requestHandler>

<searchComponent name="spellcheck" class="solr.SpellCheckComponent"/>
<searchComponent name="suggest" class="solr.SuggestComponent"/>
<searchComponent name="elevator" class="solr.QueryElevationComponent"/>
<searchComponent class="solr.HighlightComponent" name="highlight"/>

<queryResponseWriter name="json" class="solr.JSONResponseWriter"/>
<queryResponseWriter name="velocity" class="solr.VelocityResponseWriter" startup="lazy"/>
<queryResponseWriter name="xslt" class="solr.XSLTResponseWriter"/>

<updateRequestProcessorChain name="script"/>

Solr と比較して OpenSearch で注目すべき点は以下の通りです。

  • OpenSearch と Solr ともに、デフォルトのコーデックは BEST_SPEED (LZ4 圧縮アルゴリズム) です。また BEST_COMPRESSION を代替コーデックとして提供しています。さらに OpenSearch は zstd と zstd_no_dict を提供しています。各圧縮コーデックのベンチマークも活用できます。
  • ニアリアルタイム検索の場合、refresh_interval を設定する必要があります。デフォルトは 1 秒で、ほとんどのユースケースで十分です。特にバッチインデックス作成の場合、インデックス作成の速度とスループットを向上させるために、refresh_interval を 30 秒または 60 秒に増やすことをお勧めします。
  • Max boolean clause は静的設定で、indices.query.bool.max_clause_count 設定を使用してノードレベルで設定されます。
  • 明示的な requestHandler は必要ありません。すべての検索は _search または _msearch エンドポイントを使用します。デフォルト値を持つ requestHandler を使用することに慣れている場合は、検索テンプレートを使用できます。
  • /sql requestHandler の使用に慣れている場合、OpenSearch でも SQL 構文を使用してクエリを実行できます。Piped Processing Language も提供しています。
  • Spellcheck (Did-you-mean)、ハイライトはすべてクエリ実行時に利用可能です。明示的に検索コンポーネントを定義する必要はありません。
  • ほとんどの API レスポンスは JSON フォーマットで固定されています。CAT API が唯一の例外です。Solr で Velocity や XSLT が使用されている場合、アプリケーション層で管理する必要があります。CAT API は JSON、YAML、または CBOR 形式で応答します。
  • updateRequestProcessorChain については、OpenSearch はインジェストパイプラインを提供しています。インデクシング前にデータの強化や変換を行うことができます。複数のプロセッサステージをチェーンしてデータ変換のパイプラインを形成できます。プロセッサには GrokProcessor、CSVParser、JSONProcessor、KeyValue、Rename、Split、HTMLStrip、Drop、ScriptProcessor などがあります。ただし、OpenSearch の外部でデータ変換を行うことを強くお勧めします。OpenSearch Ingestion では、データ変換のための適切なフレームワークと様々な組み込みフィルターを提供しています。OpenSearch Ingestion は Data Prepper をベースに構成されているサーバーサイドのデータコレクターで、下流の分析と可視化のためにデータのフィルタリング、強化、変換、正規化、集約を行うことができます。
  • OpenSearch は、インジェストパイプラインに加えて、検索時の操作に特化した検索パイプラインを提供しています。検索パイプラインを使用することで、OpenSearch 内で検索クエリと検索結果を簡単に変換できます。現在利用可能な検索プロセッサには、フィルタークエリ、ニューラルクエリエンリッチャー、正規化、フィールド名変更、スクリプトプロセッサ、パーソナライズ検索ランキングなどがあり、今後さらに追加される予定です。

以下の画像は、refresh_interval と slowlog の設定方法を示しています。また、他の可能な設定も示しています。

スローログは以下の画像のように設定できますが、クエリフェーズとフェッチフェーズに対して別々のしきい値を設定することもできます。

設定の移行前に、現在の検索システムの運用経験とベストプラクティスに基づき、設定の調整余地があるかを評価してください。例えば、前述の例では、1 秒のスローログのしきい値はログ記録にとって負荷が高い可能性があるため、再検討する必要があるかもしれません。同様に、max.booleanClauses も見直しにより削減される可能性があります。

違い: 一部の設定は、インデックスレベルではなく、クラスターレベルまたはノードレベルで行われます。これには、最大ブール句サーキットブレーカー設定キャッシュ設定などが含まれます。

クエリの書き換え

クエリの書き換えについては、それだけでブログ記事を書けるほどの内容がありますが、ここでは OpenSearch Dashboards で利用可能なオートコンプリート機能を紹介します。これはクエリ作成を容易にするのに役立ちます。

Solr Admin UI と同様に、OpenSearch にも OpenSearch Dashboards と呼ばれる UI があります。OpenSearch Dashboards を使用して、OpenSearch クラスターを管理およびスケーリングできます。さらに、OpenSearch データの可視化、データの探索、可観測性のモニタリング、クエリの実行などの機能も提供しています。Solr UI のクエリタブに相当するものは、OpenSearch Dashboard の Dev Tools です。Dev Tools は開発環境で、OpenSearch Dashboards 環境のセットアップ、クエリの実行、データの探索、問題のデバッグなどを行うことができます。

では、以下を実行するクエリを構築してみましょう。

  1. インデックス内で “shirt” または “shoe” を検索する。
  2. ユニークな顧客数を見つけるためのファセットクエリを作成する。ファセットクエリは OpenSearch では集約クエリと呼ばれます。また、aggs クエリとしても知られています。

Solr クエリは以下のようになります。

http://localhost:8983/solr/solr_sample_data_ecommerce/select?q=shirt OR shoe
  &facet=true
  &facet.field=customer_id
  &facet.limit=-1
  &facet.mincount=1
  &json.facet={
   unique_customer_count:"unique(customer_id)"
  }

以下の画像は、上記の Solr クエリを OpenSearch クエリ DSL に書き換える方法を示しています。

結論

OpenSearch は、エンタープライズ検索、サイト検索、アプリケーション検索、e コマース検索、セマンティック検索、可観測性 (ログ可観測性、セキュリティ分析 (SIEM)、異常検出、トレース分析)、分析など、幅広いユースケースをカバーしています。Solr から OpenSearch への移行は一般的なパターンになりつつあります。このブログ記事は、そのような移行に関するガイダンスを求めるチームのための出発点となることを考えて作成されています。

OpenSearch Playground で OpenSearch をお試しいただくことが可能です。AWS にて、 OpenSearch のマネージドサービスである Amazon OpenSearch Service を使い始めることもできます。


著者について

Aswath Srinivasan iは、現在ドイツのミュンヘンを拠点とする Amazon Web Services のシニア検索エンジンアーキテクトです。様々な検索技術で 17 年以上の経験を持つ Aswath は、現在 OpenSearch に焦点を当てています。検索とオープンソースの愛好家であり、顧客や検索コミュニティの検索問題解決を支援しています。

Jon Handler カリフォルニア州パロアルトを拠点とする Amazon Web Services のシニアプリンシパルソリューションアーキテクトです。Jon は OpenSearch と Amazon OpenSearch Service と密接に連携し、AWS クラウドに移行したい検索およびログ分析ワークロードを持つ幅広い顧客に支援とガイダンスを提供しています。AWS に入社する前の Jon のソフトウェア開発者としてのキャリアには、大規模な e コマース検索エンジンの 4 年間のコーディングが含まれています。Jon はペンシルベニア大学で文学士号を、ノースウェスタン大学でコンピューターサイエンスと人工知能の理学修士号および博士号を取得しています。


本記事はソリューションアーキテクトの榎本が翻訳いたしました。原文はこちらです。