Elasticsearchに埋め込みを保存する

Elasticsearch はベクトルの保存と取得を完全にサポートしているため、埋め込みの操作に最適なデータベースになります。

フィールドタイプ

このチュートリアルの「フルテキスト検索」の章では、複数のフィールドを持つインデックスを作成する方法を学習しました。当時、Elasticsearch は、ほとんどの場合、データ自体に基づいて各フィールドに使用する最適なタイプを自動的に決定できることが述べられました。Elasticsearch 8.11 では一部のベクター型を自動的にマッピングできますが、この章では Elasticsearch の型マッピングについてさらに詳しく知る機会として、この型を明示的に定義します。

型マッピングの取得

インデックス内の各フィールドに関連付けられた型は、マッピングと呼ばれるプロセスで決定されます。マッピングは動的または明示的に行うことができます。このチュートリアルのフルテキスト検索部分で作成されたマッピングはすべて、Elasticsearch によって動的に生成されました。

Elasticsearch クライアントは、特定のインデックスに有効なタイプ マッピングを返すget_mappingメソッドを提供します。これらのマッピングを自分で調べたい場合は、Python シェルを起動して次のコードを入力します。

from app import es
es.es.indices.get_mapping(index='my_documents')

get_mapping()メソッドからの応答は、インデックス内のすべてのフィールドに関する情報を含む辞書です。便宜上、チュートリアルのフルテキスト検索セクションで作成されたmy_documentsインデックスのこの情報の、わかりやすくフォーマットされた構造を以下に示します。

{
  "my_documents": {
    "mappings": {
      "properties": {
        "category": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "content": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "created_on": {
          "type": "date"
        },
        "name": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "rolePermissions": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "summary": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "updated_at": {
          "type": "date"
        },
        "url": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
  }
}

このことから、 created_onフィールドとupdated_atフィールドは自動的にdateで型指定され、他のすべてのフィールドは型としてtextを受け取ったことがわかります。タイプを決定しようとするとき、Elasticsearch は最初にデータのタイプをチェックし、数値、ブール値、オブジェクトのタイプをフィールドに割り当てるのに役立ちます。フィールド データが文字列の場合、データが日付パターンと一致するかどうかも確認します。必要に応じて、パターンに基づく文字列の検出を数値に対しても有効にすることができます。

テキスト フィールドには、 keywordエントリを含むfields定義があります。これはサブフィールドと呼ばれ、適切な場合に使用できる代替または二次的なタイプです。Elasticsearch では、動的に型付けされたtextフィールドにkeywordサブフィールドが割り当てられます。特定のカテゴリの正確な検索を実行するために、 category.keywordサブフィールドがすでに使用されています。サブフィールドが追加されないようにするには、 textまたはkeywordの明示的なマッピングを指定すると、これがメインの唯一のタイプになります。

インデックスにベクトル場を追加する

各ドキュメントの埋め込みが保存されるインデックスに新しいフィールドを追加しましょう。

明示的なマッピングの構造は、Elasticsearch クライアントのget_mapping()メソッドによって返される応答のmappingsキーと一致します。マッピングに含まれていないフィールドはこれまでと同様に動的に入力され続けるため、明示的に入力する必要があるフィールドのみを指定する必要があります。

以下に、 Searchクラスのcreate_index()メソッドの新しいバージョンを示します。このバージョンでは、 embeddingという名前の明示的に型指定されたフィールドが追加されています。search.py のこのメソッドを置き換えます:

class Search:
    # ...

    def create_index(self):
        self.es.indices.delete(index='my_documents', ignore_unavailable=True)
        self.es.indices.create(index='my_documents', mappings={
            'properties': {
                'embedding': {
                    'type': 'dense_vector',
                }
            }
        })

ご覧のとおり、 embeddingフィールドには、埋め込みを格納するときに適切な型であるdense_vector型が指定されています。後ほど、他の種類のセマンティック検索アプリケーションで役立つ別の種類のベクトルsparse_vectorについて学習します。

dense_vector型はいくつかのパラメータを受け入れますが、それらはすべてオプションです。

  • dims: 保存されるベクトルのサイズ。バージョン 8.11 以降では、最初のドキュメントが挿入されると、寸法が自動的に割り当てられます。

  • index: 検索用にベクトルをインデックスする必要があることを示すには、 Trueに設定する必要があります。これがデフォルトです。

  • similarity: ベクトルを比較するときに使用する距離関数。最も一般的な 2 つはdot_productcosineです。ドット積はより効率的ですが、ベクトルを正規化する必要があります。デフォルトはcosineです。

ドキュメントへの埋め込みの追加

前のセクションでは、SentenceTransformers フレームワークとall-MiniLM-L6-v2モデルを使用して埋め込みを生成する方法を学習しました。ここで、モデルをアプリケーションに統合します。

まず、モデルはSearchクラスのコンストラクターでインスタンス化できます。

# ...
from sentence_transformers import SentenceTransformer

# ...

class Search:
    def __init__(self):
        self.model = SentenceTransformer('all-MiniLM-L6-v2')
        self.es = Elasticsearch(cloud_id=os.environ['ELASTIC_CLOUD_ID'],
                                api_key=os.environ['ELASTIC_API_KEY'])
        client_info = self.es.info()
        print('Connected to Elasticsearch!')
        pprint(client_info.body)
      
    # ...

このチュートリアルの全文検索の部分で説明したように、 Searchクラスには、それぞれ単一のドキュメントと複数のドキュメントをインデックスに挿入するためのinsert_document()メソッドとinsert_documents()メソッドがあります。これら 2 つの方法では、各ドキュメントに対応する埋め込みを生成する必要があります。

次のコード ブロックは、これら 2 つのメソッドの新しいバージョンと、埋め込みを返す新しいget_embedding()ヘルパー メソッドを示しています。

class Search:
    # ...

    def get_embedding(self, text):
        return self.model.encode(text)

    def insert_document(self, document):
        return self.es.index(index='my_documents', document={
            **document,
            'embedding': self.get_embedding(document['summary']),
        })

    def insert_documents(self, documents):
        operations = []
        for document in documents:
            operations.append({'index': {'_index': 'my_documents'}})
            operations.append({
                **document,
                'embedding': self.get_embedding(document['summary']),
            })
        return self.es.bulk(operations=operations)

変更されたメソッドは、挿入されるドキュメントに新しいembeddingフィールドを追加します。埋め込みは各ドキュメントのsummaryフィールドから生成されます。一般に、埋め込みは文または短い段落から生成されるため、この場合は要約が使用するのに理想的なフィールドです。他のオプションとしては、ドキュメントのタイトルを含むnameフィールド、またはドキュメントのbodyの最初の数文などがあります。

これらの変更を行うと、インデックスを再構築して、各ドキュメントの埋め込みを保存できるようになります。インデックスを再構築するには、次のコマンドを使用します。

flask reindex

念のためお知らせしますが、 flask reindexコマンドはapp.pyreindex()関数に実装されています。これは、 Searchクラスのreindex()メソッドを呼び出し、次にcreate_index()を呼び出して、 data.jsonファイルのすべてのデータをinsert_documents()に渡します。

最先端の検索体験を構築する準備はできましたか?

十分に高度な検索は 1 人の努力だけでは実現できません。Elasticsearch は、データ サイエンティスト、ML オペレーター、エンジニアなど、あなたと同じように検索に情熱を傾ける多くの人々によって支えられています。ぜひつながり、協力して、希望する結果が得られる魔法の検索エクスペリエンスを構築しましょう。

はじめましょう