ブログ

Elasticsearchを埋め込みストアとして使用したLangChain4j

LangChain4j (LangChain for Java) には埋め込みストアとして Elasticsearch があります。これを使用して、プレーン Java で RAG アプリケーションを構築する方法を説明します。

前回の投稿では、LangChain4j とは何か、またどのように使用するかについて説明しました。

  • LLMとChatLanguageModelを実装して議論する ChatMemory

  • チャット履歴をメモリに保持して、LLMとの以前の議論の文脈を思い出す

このブログ投稿では、次の方法について説明します。

  • テキスト例からベクトル埋め込みを作成する

  • ベクトル埋め込みをElasticsearch埋め込みストアに保存する

  • 類似ベクトルを検索

埋め込みを作成する

埋め込みを作成するには、使用するEmbeddingModelを定義する必要があります。たとえば、前回の投稿で使用したのと同じミストラル モデルを使用できます。それは ollama で実行されていました:

EmbeddingModel model = OllamaEmbeddingModel.builder()
  .baseUrl(ollama.getEndpoint())
  .modelName(MODEL_NAME)
  .build();

モデルはテキストからベクトルを生成できます。ここで、モデルによって生成された次元の数を確認できます。

Logger.info("Embedding model has {} dimensions.", model.dimension());
// This gives: Embedding model has 4096 dimensions.

テキストからベクトルを生成するには、以下を使用します。

Response<Embedding> response = model.embed("A text here");

または、テキスト、価格、発売日などに基づいてフィルタリングできるようにメタデータも提供したい場合は、 Metadata.from()使用できます。たとえば、ここではゲーム名をメタデータ フィールドとして追加しています。

TextSegment game1 = TextSegment.from("""
    The game starts off with the main character Guybrush Threepwood stating "I want to be a pirate!"
    To do so, he must prove himself to three old pirate captains. During the perilous pirate trials, 
    he meets the beautiful governor Elaine Marley, with whom he falls in love, unaware that the ghost pirate 
    LeChuck also has his eyes on her. When Elaine is kidnapped, Guybrush procures crew and ship to track 
    LeChuck down, defeat him and rescue his love.
""", Metadata.from("gameName", "The Secret of Monkey Island"));
Response<Embedding> response1 = model.embed(game1);
TextSegment game2 = TextSegment.from("""
    Out Run is a pseudo-3D driving video game in which the player controls a Ferrari Testarossa 
    convertible from a third-person rear perspective. The camera is placed near the ground, simulating 
    a Ferrari driver's position and limiting the player's view into the distance. The road curves, 
    crests, and dips, which increases the challenge by obscuring upcoming obstacles such as traffic 
    that the player must avoid. The object of the game is to reach the finish line against a timer.
    The game world is divided into multiple stages that each end in a checkpoint, and reaching the end 
    of a stage provides more time. Near the end of each stage, the track forks to give the player a 
    choice of routes leading to five final destinations. The destinations represent different 
    difficulty levels and each conclude with their own ending scene, among them the Ferrari breaking 
    down or being presented a trophy.
""", Metadata.from("gameName", "Out Run"));
Response<Embedding> response2 = model.embed(game2);

このコードを実行する場合は、 Step5EmbedddingsTest.javaクラスをチェックアウトしてください。

ベクトルを保存するためにElasticsearchを追加する

LangChain4j はメモリ内の埋め込みストアを提供します。これは簡単なテストを実行するのに便利です:

EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.add(response1.content(), game1);
embeddingStore.add(response2.content(), game2);

しかし、明らかに、このデータストアはすべてをメモリに保存し、サーバーに無限のメモリがないため、これよりもはるかに大きなデータセットでは機能しません。そのため、代わりに、定義上「弾性」があり、データに合わせてスケールアップおよびスケールアウトできる Elasticsearch に埋め込みを保存することができます。そのためには、Elasticsearch をプロジェクトに追加しましょう。

<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-elasticsearch</artifactId>
  <version>${langchain4j.version}</version>
</dependency>

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>elasticsearch</artifactId>
  <version>1.20.1</version>
  <scope>test</scope>
</dependency>

お気づきのとおり、Elasticsearch TestContainers モジュールもプロジェクトに追加したので、テストから Elasticsearch インスタンスを起動できます。

// Create the elasticsearch container
ElasticsearchContainer container =
  new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:8.15.0")
    .withPassword("changeme");

// Start the container. This step might take some time...
container.start();

// As we don't want to make our TestContainers code more complex than
// needed, we will use login / password for authentication.
// But note that you can also use API keys which is preferred.
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "changeme"));

// Create a low level Rest client which connects to the elasticsearch container.
client = RestClient.builder(HttpHost.create("https://" + container.getHttpHostAddress()))
  .setHttpClientConfigCallback(httpClientBuilder -> {
    httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
    httpClientBuilder.setSSLContext(container.createSslContextFromCa());
    return httpClientBuilder;
  })
  .build();

// Check the cluster is running
client.performRequest(new Request("GET", "/"));

Elasticsearch を埋め込みストアとして使用するには、LangChain4j のインメモリ データストアから Elasticsearch データストアに切り替えるだけです。

EmbeddingStore<TextSegment> embeddingStore =
  ElasticsearchEmbeddingStore.builder()
    .restClient(client)
    .build();
embeddingStore.add(response1.content(), game1);
embeddingStore.add(response2.content(), game2);

これにより、Elasticsearch のdefaultインデックスにベクトルが保存されます。インデックス名をより意味のあるものに変更することもできます。

EmbeddingStore<TextSegment> embeddingStore =
  ElasticsearchEmbeddingStore.builder()
    .indexName("games")
    .restClient(client)
    .build();
embeddingStore.add(response1.content(), game1);
embeddingStore.add(response2.content(), game2);

このコードを実行する場合は、 Step6ElasticsearchEmbedddingsTest.javaクラスをチェックアウトしてください。

類似ベクトルを検索

類似のベクトルを検索するには、まず、以前使用したのと同じモデルを使用して、質問をベクトル表現に変換する必要があります。すでにそれを行ったので、もう一度これを行うのは難しくありません。この場合、メタデータは必要ないことに注意してください。

String question = "I want to pilot a car";
Embedding questionAsVector = model.embed(question).content();

質問をこのように表現して検索リクエストを作成し、埋め込みストアに最初の上位ベクトルを見つけるように依頼することができます。

EmbeddingSearchResult<TextSegment> result = embeddingStore.search(
  EmbeddingSearchRequest.builder()
    .queryEmbedding(questionAsVector)
    .build());

これで、結果を反復処理して、メタデータから取得したゲーム名やスコアなどの情報を出力できます。

result.matches().forEach(m -> Logger.info("{} - score [{}]",
  m.embedded().metadata().getString("gameName"), m.score()));

予想どおり、最初のヒットは「Out Run」になります。

アウトラン
Out Run - score [0.86672974]
The Secret of Monkey Island - score [0.85569763]

このコードを実行する場合は、 Step7SearchForVectorsTest.javaクラスをチェックアウトしてください。

舞台裏

Elasticsearch Embedding ストアのデフォルト構成では、バックグラウンドで近似 kNN クエリが使用されます。

POST games/_search
{
  "query" : {
    "knn": {
      "field": "vector",
      "query_vector": [-0.019137882, /* ... */, -0.0148779955]
    }
  }
}

ただし、埋め込みストアにデフォルトの構成 ( ElasticsearchConfigurationKnn ) とは別の構成 ( ElasticsearchConfigurationScript ) を提供することでこれを変更できます。

EmbeddingStore<TextSegment> embeddingStore =
  ElasticsearchEmbeddingStore.builder()
    .configuration(ElasticsearchConfigurationScript.builder().build())
    .indexName("games")
    .restClient(client)
    .build();

ElasticsearchConfigurationScript実装は、script_score cosineSimilarity関数 を使用して、バックグラウンドで クエリ を実行します。

基本的に、電話をかけるときは:

EmbeddingSearchResult<TextSegment> result = embeddingStore.search(
  EmbeddingSearchRequest.builder()
    .queryEmbedding(questionAsVector)
    .build());

これにより、次のコードが呼び出されます。

POST games/_search
{
  "query": {
    "script_score": {
      "script": {
        "source": "(cosineSimilarity(params.query_vector, 'vector') + 1.0) / 2",
        "params": {
          "queryVector": [-0.019137882, /* ... */, -0.0148779955]
        }
      }
    }
  }
}

この場合、結果は「順序」の点では変化せず、スコアのみが調整されます。これは、 cosineSimilarity呼び出しでは近似値を使用せず、一致するベクトルごとにコサインを計算するためです。

Out Run - score [0.871952]
The Secret of Monkey Island - score [0.86380446]

このコードを実行する場合は、 Step7SearchForVectorsTest.javaクラスをチェックアウトしてください。

まとめ

テキストから埋め込みを簡単に生成する方法と、2 つの異なるアプローチを使用して Elasticsearch で最も近い近傍を保存および検索する方法について説明しました。

  • デフォルトのElasticsearchConfigurationKnnオプションを使用して、近似高速knnクエリを使用する

  • 正確だが遅いscript_scoreクエリをElasticsearchConfigurationScriptオプションとともに使用する

次のステップでは、ここで学んだことを基に、完全な RAG アプリケーションを構築します。

関連記事

モックと実際のElasticsearchを使用してJavaコードをテストする

Piotr Przybyl

LangChain4j を導入して、LLM の Java アプリケーションへの統合を簡素化します。

David Pilato

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

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

はじめましょう