Elasticsearch
Maven Dependency
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-elasticsearch</artifactId>
<version>1.20.0-beta30</version>
</dependency>
Overview
The langchain4j-elasticsearch module provides integration with Elasticsearch as an embedding store and content
retriever.
It comes with two main classes:
ElasticsearchEmbeddingStore: an implementation of theEmbeddingStoreinterface that uses Elasticsearch to store and retrieve embeddings.ElasticsearchContentRetriever: an implementation of theContentRetrieverinterface that uses Elasticsearch to retrieve relevant documents based on vector similarity search.
Both classes need an Elasticsearch Client to connect to the Elasticsearch server.
String apiKey = "VnVhQ2ZHY0JDZGJrU...";
ElasticsearchClient client = ElasticsearchClient.of(ec -> ec
.host("https://localhost:9200")
.apiKey(apiKey));
Note:
See the Elasticsearch documentation on how to create an ElasticsearchClient instance.
ElasticsearchEmbeddingStore
To create the ElasticsearchEmbeddingStore instance, you need to provide an ElasticsearchClient:
ElasticsearchEmbeddingStore store = ElasticsearchEmbeddingStore.builder()
.client(client)
.build();
It comes with the following options:
indexName: the name of the Elasticsearch index to use. Default isdefault.configuration: theElasticsearchConfigurationto use. Default isElasticsearchConfigurationKnn.refresh: when documents written or removed by ID become visible to search. Default isRefresh.False.
The previous code is equivalent to:
ElasticsearchEmbeddingStore store = ElasticsearchEmbeddingStore.builder()
.client(client)
.configuration(ElasticsearchConfigurationKnn.builder().build())
.indexName("default")
.build();
Refresh policy
Elasticsearch does not make a document searchable the moment the write request returns. By default a document becomes visible to search on the next periodic index refresh (once per second, unless the index says otherwise). Until then the document is safely stored and can be fetched by ID, but it will not show up in a search.
This matters when you add documents and then immediately do something that searches for them. The refresh option lets
you wait for search visibility instead:
import co.elastic.clients.elasticsearch._types.Refresh;
ElasticsearchEmbeddingStore store = ElasticsearchEmbeddingStore.builder()
.client(client)
.refresh(Refresh.WaitFor)
.build();
The three values are:
Refresh.False(default) returns as soon as the document is stored, and leaves refreshing to Elasticsearch. This is the fastest option.Refresh.WaitForreturns once the document is visible to search. It does not force extra refreshes, so it is usually the right choice when you need visibility. Note that if the index has automatic refreshing turned off (index.refresh_interval: -1), the call waits until something else triggers a refresh.Refresh.Trueforces a refresh immediately. This gives visibility without waiting, but creating a new segment on every request reduces indexing throughput, so avoid it on write-heavy indices.
The option applies to add, addAll and removeAll(Collection<String> ids). It does not change searches, and it does
not isolate you from writes made concurrently by someone else. The same option is available on
ElasticsearchContentRetriever.builder().
Filtered removal (removeAll(Filter)) is a delete-by-query, so it too only matches documents that are already
visible to search: embeddings added moments earlier can survive it. If you add embeddings and then immediately remove
them by filter, configure Refresh.WaitFor so the writes are searchable before the removal runs.
See the Elasticsearch refresh parameter documentation for details.
Storing documents without an embedding
Next to the usual add(Embedding, TextSegment) methods, the store can also index plain text, without computing an
embedding for it:
store.add("Printer troubleshooting guide"); // generates an id
store.add("my-id", "Printer troubleshooting guide"); // with your own id
store.addAllText(List.of("First guide", "Second guide")); // several at once
Because these documents have no vector, vector search never returns them, neither with
ElasticsearchConfigurationKnn nor with
ElasticsearchConfigurationScript. They are still found by full text search, so a
single index can hold both embedded and text-only documents, and you can search it both ways with
ElasticsearchConfigurationFullText or
ElasticsearchConfigurationHybrid.
ElasticsearchContentRetriever
A ContentRetriever needs an embedding model:
EmbeddingModel embeddingModel = new AllMiniLmL6V2QuantizedEmbeddingModel();
To create an ElasticsearchContentRetriever instance, you need to provide the ElasticsearchClient and
the EmbeddingModel:
ElasticsearchContentRetriever contentRetriever = ElasticsearchContentRetriever.builder()
.client(client)
.embeddingModel(embeddingModel)
.build();
It comes with the following options:
configuration: theElasticsearchConfigurationto use (see below). Default isElasticsearchConfigurationKnn.indexName: the name of the Elasticsearch index to use. Default isdefault. Index will be created automatically if not exists.maxResults: the maximum number of results to retrieve. Default is3.minScore: the minimum score threshold for retrieved results. Default is0.0.filter: aFilterto apply during retrieval if any. Default isnull.
The previous code is equivalent to:
ElasticsearchContentRetriever contentRetriever = ElasticsearchContentRetriever.builder()
.client(client)
.embeddingModel(embeddingModel)
.configuration(ElasticsearchConfigurationKnn.builder().build())
.indexName("default")
.maxResults(3)
.minScore(0.0)
.filter(null)
.build();
ElasticsearchConfiguration
An ElasticsearchConfiguration defines how the embedding store or content retriever will interact with the
Elasticsearch server. You can create your own configuration by implementing the ElasticsearchConfiguration interface,
or use one of the provided implementations:
ElasticsearchConfigurationKnn: uses approximate kNN queries (default).ElasticsearchConfigurationScript: uses scriptScore queries. Note that this implementation is using cosine similarity.ElasticsearchConfigurationFullText: uses full text search (for content retriever only).ElasticsearchConfigurationHybrid: uses hybrid search (for content retriever only, requires paid license). It combines a kNN vector query with a full text query.
To create a configuration instance, you can use the builder provided by each implementation. For example:
ElasticsearchConfiguration configuration = ElasticsearchConfigurationKnn.builder().build();
ElasticsearchConfigurationKnn
The ElasticsearchConfigurationKnn uses approximate kNN queries to perform vector similarity search.
It is the default configuration used by both ElasticsearchEmbeddingStore
and ElasticsearchContentRetriever.
To create an instance, you can use the builder:
ElasticsearchConfiguration configuration = ElasticsearchConfigurationKnn.builder().build();
It comes with the following options:
numCandidates: the number of candidate neighbors to consider during the search. Default isnull, meaning using the default Elasticsearch value.includeVectorResponse: whether to include vector fields in the search response. Default isfalse.
Note: From version 9.2 of the elasticsearch server, vector fields are excluded from the response by default. To include vector fields in the responses (not recommended), set the
includeVectorResponsein the builder:ElasticsearchConfigurationKnn configuration = ElasticsearchConfigurationKnn.builder().includeVectorResponse(true).build();
ElasticsearchConfigurationScript
The ElasticsearchConfigurationScript uses scriptScore queries to perform vector similarity search. Note that this
implementation is using cosine similarity.
It is available for both ElasticsearchEmbeddingStore
and ElasticsearchContentRetriever.
To create an instance, you can use the builder:
ElasticsearchConfiguration configuration = ElasticsearchConfigurationScript.builder().build();
It comes with the following options:
includeVectorResponse: whether to include vector fields in the search response. Default isfalse.
Note: From version 9.2 of the elasticsearch server, vector fields are excluded from the response by default. To include vector fields in the responses (not recommended), set the
includeVectorResponsein the builder:ElasticsearchConfiguration configuration = ElasticsearchConfigurationScript.builder().includeVectorResponse(true).build();
ElasticsearchConfigurationFullText
The ElasticsearchConfigurationFullText uses full text search to retrieve relevant documents.
It is available ElasticsearchContentRetriever only.
To create an instance, you can use the builder:
ElasticsearchConfiguration configuration = ElasticsearchConfigurationFullText.builder().build();
ElasticsearchConfigurationHybrid
The ElasticsearchConfigurationHybrid uses hybrid search to combine a kNN vector query with a full text query. Note
that hybrid search requires an elasticsearch enterprise license or a trial.
It is available ElasticsearchContentRetriever only.
To create an instance, you can use the builder:
ElasticsearchConfiguration configuration = ElasticsearchConfigurationHybrid.builder().build();
It comes with the following options:
numCandidates: the number of candidate neighbors to consider during the search. Default isnull, meaning using the default Elasticsearch value.includeVectorResponse: whether to include vector fields in the search response. Default isfalse.
Note: From version 9.2 of the elasticsearch server, vector fields are excluded from the response by default. To include vector fields in the responses (not recommended), set the
includeVectorResponsein the builder:ElasticsearchConfiguration configuration = ElasticsearchConfigurationHybrid.builder().includeVectorResponse(true).build();
Creating Custom Configurations
You can create your own Elasticsearch configuration by implementing the ElasticsearchConfiguration interface. For example:
public class MyElasticsearchConfiguration implements ElasticsearchConfiguration {
@Override
SearchResponse<Document> vectorSearch(
ElasticsearchClient client,
String indexName,
EmbeddingSearchRequest embeddingSearchRequest) {
// Your optional custom vector search implementation here
}
@Override
SearchResponse<Document> fullTextSearch(
ElasticsearchClient client,
String indexName,
FullTextSearchRequest request) {
// Your optional custom full text search implementation here
}
@Override
SearchResponse<Document> hybridSearch(
ElasticsearchClient client,
String indexName,
EmbeddingSearchRequest embeddingSearchRequest,
String textQuery) {
// Your optional custom hybrid search implementation here
}
}
Please note that you can implement only the methods relevant to your use case:
vectorSearchfor vector similarity search (used by bothElasticsearchEmbeddingStoreandElasticsearchContentRetriever).fullTextSearchfor full text search (used byElasticsearchContentRetrieveronly).hybridSearchfor hybrid search (used byElasticsearchContentRetrieveronly).
The FullTextSearchRequest carries the textQuery to search for, together with the maxResults, minScore and
filter configured on the ElasticsearchContentRetriever. Your implementation is responsible for applying them,
otherwise documents which do not match the filter can be returned.
Note: There is also a deprecated
fullTextSearch(ElasticsearchClient client, String indexName, String textQuery)method. Configurations which only implement that one keep working, but themaxResults,minScoreandfilterof the retriever are ignored, and a warning is logged. Please implement the method taking aFullTextSearchRequestinstead.