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 アプリケーションを構築します。

