<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Java - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Java - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/blog/category/java-programming</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/java-programming</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/java-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sun, 20 Sep 2026 00:35:35 GMT</lastBuildDate>
  <item>
    <title><![CDATA[New Elasticsearch ES|QL plugin for IntelliJ IDEA]]></title>
    <description><![CDATA[Build and run Elasticsearch ES|QL queries in your IDE with the new plugin for IntelliJ IDEA.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/docs/reference/query-languages/esql">Elasticsearch Query Language (ES|QL)</a> is Elasticsearch’s piped query language, designed for intuitive data querying and manipulation. Refer to our <a href="https://www.elastic.co/blog/getting-started-elasticsearch-query-language">getting started guide</a> to learn more.</p><p>The Elasticsearch Java client <a href="https://www.elastic.co/search-labs/blog/esql-queries-to-java-objects">supports ES|QL queries</a> through the DSL, but currently it treats queries as simple strings, with no dedicated helper; and while <a href="https://www.elastic.co/kibana">Kibana</a> offers an excellent <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql-kibana">UI to build ES|QL queries</a>, we’re aware that sometimes having everything needed to write applications in the integrated development environment (IDE) offers a better experience. So, until the Java client extends its type support to ES|QL, we wrote an Intellij IDEA plugin that autocompletes, syntax checks, shows documentation, and executes ES|QL queries.</p><p>The plugin currently supports Java, Kotlin, and plain text files, in case the Java Virtual Machine (JVM) isn’t your thing.</p><p>Check it out in the <a href="https://plugins.jetbrains.com/plugin/28898-elasticsearch-es-ql">JetBrains Marketplace page</a> and in the <a href="https://github.com/elastic/esql-idea-plugin">GitHub repository</a>, for more information.</p><h2>Prerequisites</h2><ul><li><p>IDE: Intellij IDEA version &gt;= 253 (community or ultimate)</p></li></ul><h2>Usage</h2><p>Install the plugin in Intellij IDEA like you would with every other plugin, so either from the <a href="https://plugins.jetbrains.com/plugin/28898-elasticsearch-es-ql">JetBrains marketplace</a> or by going to Settings -&gt; Plugins -&gt; Marketplace and searching “esql”.</p><p>The following examples are written using Java, but Kotlin is also supported and the usage is pretty much the same.</p><p>Create a text block string, write “ES|QL” in a simple comment above it, and you’re done.</p>// ES|QL
String query = """
""";<p>If you see the Elastic logo appearing on the left:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt987846e694123956/6a170bc8dc55decc5ce00e21/620d47b0c241271ab9bf727c37d3ab5f4137ca44-417x55.png" alt="Code editor showing an ES|QL comment and a string variable being initialized for a query, with the Elastic icon in the gutter." /><p>then everything is working, and you’re ready to write your queries.</p><p>Why text blocks and not simple strings? The ES|QL syntax accepts quotes in various contexts, and escaping them would trigger other errors in the syntax checker, so we decided on text blocks to keep things simple.</p><p>It’s even simpler for txt files, as you can just add the comment and start writing the query right below:</p><h3>Connecting to a server instance</h3><p>The plugin can be connected to an Elasticsearch server instance to fetch indices and field names, which will then be added to the autocompletion options. Look for the Elastic logo on the bottom left of of the screen (or wherever you keep your tools), and configure your connection to any server instance:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5069560e513f3ff7/6a170bcaa29299108cd0104e/9f5109542c921ad523458b9156551bf1fca7d41a-418x269.png" alt="Elasticsearch connection panel showing a “local” dropdown, status marked “Not connected,” and a Connect button in a dark-themed interface." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf73cededd56ba9cc/6a170bcba6c2b93aabe79739/9fe37a53d97f67cbdd8623793bce768e8e2f9ced-577x298.png" alt="Dialog box for adding an Elasticsearch connection, showing fields for name, URL, API key, refresh rate, and buttons to test or confirm the connection." /><h3>Autocomplete</h3><p>Start typing while in the text block to automatically open the autocompletion popup, which will return a list of acceptable commands/values to continue writing the query correctly. If you want to manually trigger autocompletion, <code>ctlr+space</code> is the IDE’s shortcut to use:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt229d9a3bc33091f4/6a170bcc60084b84183c4590/987a927ab0e682bb1f9d07c934dd4254a769db20-584x252.png" alt="Java editor showing an ES|QL query with an autocomplete menu listing keywords like WHERE, STATS, DISSECT, FORK, and KEEP." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1359cdac9ed83fa4/6a170bce961e6982e1c4cf4e/5f8e379ea1cca345b38d0b7a0c2a873db6de624f-584x252.png" alt="Java editor showing an ES|QL query with an autocomplete panel listing field suggestions such as field, name, title, vector, and string." /><h3>Syntax check</h3><p>The plugin will highlight errors in queries, explaining what to fix:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc935a693e4fcf2eb/6a170bcf67045b724745c216/15e91eab6c00fb49cfa5c1c6e270beeba534afc3-812x252.png" alt="Java editor showing an ES|QL query with an invalid keyword after a pipe operator and a tooltip explaining the syntax error." /><h3>Documentation</h3><p>Hovering with the cursor over commands will display documentation describing what the command can be used for and its correct syntax:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4aca4151e89f1d1b/6a170bd167045b597945c21a/c78295103c2188a3615edb2e001acbaf17523656-1072x627.png" alt="ava editor showing an ES|QL query alongside a documentation panel explaining how the WHERE clause works, including syntax, parameters, and examples." /><h3>Running the query</h3><p>Once connected to a server instance, you can run queries by clicking on the green button beside the Elastic icon: The results will be displayed in the tool window:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt96849d8f88f75832/6a170bd38b73cb7e2118a066/bff17bee7269dfb6f30140c9cd78dc203990f21c-1130x441.png" alt="IDE window showing an ES|QL query in a text file and a results panel below it listing returned book records with columns like author, title, year, and ID." /><p>Or if you’re writing an application, you can use the Java client like so:</p>// ES|QL
String query = """
	FROM my-index
| SORT year DESC
| LIMIT 10
""";

try (ElasticsearchClient client = ElasticsearchClient.of(e -&gt; e
                .host(serverUrl)
                .apiKey(apiKey))) {

client.esql().query(QueryRequest.of(qr -&gt; qr.query(query)));

}<p>Check our previous <a href="https://www.elastic.co/search-labs/blog/esql-queries-to-java-objects">ES|QL Java Client article</a> for a complete example of mapping ES|QL results to Java objects.</p><h2>How does it work?</h2><p>There’s no AI involved; the plugin is based on the ES|QL <a href="https://www.antlr.org/">ANTLR</a> grammar for autocompletion and syntax check, and it uses the <a href="https://www.elastic.co/docs/reference/query-languages/esql">Elasticsearch docs</a> to show documentation.</p><h2>Conclusion</h2><p>The plugin is still experimental, so feel free to report any bug or feature request on the <a href="https://github.com/elastic/esql-idea-plugin">Github repository</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/esql-plugin-intellij-idea</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/esql-plugin-intellij-idea</guid>
    <category><![CDATA[ES|QL]]></category>
    <category><![CDATA[Java]]></category>
    <dc:creator><![CDATA[Laura Trotta]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt050f40a347c2ebb8/6a170bd414b2706a45e3c644/91366de35a1b66860ce0d126c8a83e5b25b678f0-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Testing Elasticsearch. It just got simpler.]]></title>
    <description><![CDATA[Explaining how Elasticsearch integration tests have become simpler thanks to improvements in Elasticsearch 9.x, the modern Java client, and Testcontainers 2.x.]]></description>
    <content:encoded><![CDATA[<p>When I first wrote about <a href="https://www.elastic.co/search-labs/blog/series/integration-tests-using-elasticsearch">testing Elasticsearch</a> with Testcontainers for Java, the focus was very pragmatic: if you care about correctness, you should test against a real node; if you care about confidence, your integration tests should resemble production as closely as possible; and if you care about maintainability, your setup shouldn’t turn into a maze of mocks and assumptions.</p><p>That philosophy hasn’t changed.</p><p>What has changed, however, is how little effort it now takes to achieve that goal. With Elasticsearch 9.x, the modern Java client, and Testcontainers 2.x, the experience of writing integration tests feels noticeably smoother, as if a layer of incidental complexity has quietly been removed.</p><p>The example accompanying this article is intentionally modest and can be found <a href="https://github.com/pioorg/elasticsearch9-testcontainers2/blob/main/src/test/java/testing_elasticsearch/ES9TC2DemoTest.java">here</a>.</p><p>It doesn’t attempt to demonstrate sophisticated indexing strategies or elaborate data pipelines; instead, it concentrates on the essentials, because the essentials are precisely where the improvements are most visible.</p><h2>When the tooling stops getting in the way</h2><p>Anyone who has maintained a test suite for a few years will recognize the pattern: You introduce a new library, a transitive dependency pulls something unexpected, and before long, you’re negotiating between versions of testing engines rather than writing tests.</p><p>With Testcontainers 2.x, that negotiation largely disappears. The dependency structure is clearer, the modules are more explicit, and the accidental coupling to older testing frameworks no longer sneaks in behind your back. In practical terms, adding Elasticsearch support to your tests is now as straightforward as declaring:</p>&lt;dependency&gt;
  &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
  &lt;artifactId&gt;testcontainers-elasticsearch&lt;/artifactId&gt;
  &lt;version&gt;2.0.3&lt;/version&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;<p>And, if you’re using JUnit Jupiter integration:</p>&lt;dependency&gt;
  &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
  &lt;artifactId&gt;testcontainers-junit-jupiter&lt;/artifactId&gt;
  &lt;version&gt;2.0.3&lt;/version&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;<p>There are no exclusions to sprinkle in, no legacy engines to silence, and no uneasy feeling that something hidden might surface during the next upgrade. The configuration becomes almost unremarkable, which, in the context of build tooling, is a compliment.</p><h2>A real Elasticsearch node, with security intact</h2><p>In the demo test, we use the official Elasticsearch 9.3.1 Docker image:</p>var container =
    new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:9.3.1");

container.start();<p>At first glance, this may look similar to older examples, yet the subtle difference lies in what we no longer need to do. <strong>We don’t disable security.</strong> <strong>We don’t bypass SSL.</strong> We don’t simplify the environment just to make the test convenient.</p><p>Instead, once the container is started, we construct a client that uses the REST API and authenticates properly:</p>try (var client = ElasticsearchClient.of(c -&gt; c
     .host("https://" + container.getHttpHostAddress())
     .usernameAndPassword("elastic", ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
     .sslContext(container.createSslContextFromCa())
)) {<p>What deserves special mention here is how neat the client construction itself has become. In earlier iterations, creating an Elasticsearch client often meant juggling multiple intermediate objects, configuring transport layers explicitly, wrapping low-level clients, and dedicating some amount of code to what was essentially plumbing. Now, the signal-to-noise ratio is refreshingly high. The builder encapsulates the necessary details, the container provides what the client needs, and the resulting configuration fits comfortably within a few readable lines.</p><p>Just as importantly, the <code>ElasticsearchClient</code> is <code>AutoCloseable</code>, which means it integrates naturally with try-with-resources, ensuring proper cleanup without additional ceremony. The lifecycle is explicit, concise, and self-contained, which is exactly what you want in integration tests that should focus on behavior rather than infrastructure management.</p><p>The container exposes everything required to build a legitimate, secure connection, and the client integrates with it naturally, which means the test environment mirrors production in all the aspects that matter, without imposing additional mental overhead from the developer.</p><p>This alignment between realism and simplicity is, perhaps, one of the most meaningful improvements.</p><h2>Typed APIs change the character of tests</h2><p>The evolution of the Elasticsearch Java client has also reshaped how integration tests read and feel. Where older approaches often involved parsing JSON responses or navigating loosely typed structures, the modern client offers a builder-based, strongly typed API that guides you through valid request shapes at compile time.</p><p>In the demo, we perform a simple cluster health check:</p>var health = client.cluster().health();

Assertions.assertEquals("docker-cluster", health.clusterName());
Assertions.assertEquals(HealthStatus.Green, health.status());<p>What’s striking here is not the complexity of the operation, but the absence of friction. There’s no manual extraction from maps, no assertions built on untyped string values, and no detour into low-level response handling. The test code looks indistinguishable from application code, which subtly reinforces the idea that integration tests aren’t a special category of code with different rules, but simply another consumer of the same APIs.</p><p>When the boundary between production code and test code becomes thinner, confidence increases almost by default.</p><h2>Reading the test as a story</h2><p>If you take a look at the full test case:</p>@Test
void newClientTest() throws IOException {
    try (var container =
             new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:9.3.1")) {
        
        container.start();
        
        try (
            var client = ElasticsearchClient.of(c -&gt;
                c.host("https://" + container.getHttpHostAddress())
                    .usernameAndPassword("elastic", ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
                    .sslContext(container.createSslContextFromCa()))) {

            HealthResponse health = client.cluster().health();

            Assertions.assertEquals("docker-cluster", health.clusterName());
            Assertions.assertEquals(HealthStatus.Green, health.status());
        }
    }
}<p>you’ll notice that it reads less like a configuration script and more like a short narrative:</p><ul><li><p>We define the container.</p></li><li><p>We start the container.</p></li><li><p>We build a client.</p></li><li><p>We call a real API.</p></li><li><p>We assert the outcome.</p></li></ul><p>The supporting infrastructure fades into the background, leaving the intent of the test clearly visible. That clarity isn’t accidental; it’s the cumulative effect of incremental improvements across Testcontainers and the Elasticsearch client.</p><h2>The advanced patterns still apply</h2><p>None of the more advanced techniques discussed in earlier articles, <a href="https://www.elastic.co/search-labs/blog/elasticsearch-integration-tests-faster">Faster integration tests with real Elasticsearch</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-improve-performance-integration-tests">Advanced integration tests with real Elasticsearch</a>, have become obsolete. Reusing containers to speed up large test suites, customizing cluster settings, preloading indices, or testing role-based access scenarios remain entirely valid and, in many cases, essential.</p><p>What has improved is the baseline experience. The simplest possible integration test, the one that merely needs a real node and a real client, no longer requires defensive configuration or dependency gymnastics. It’s concise, expressive, and production-like by default.</p><h2>Progress without drama</h2><p>There was no dramatic rewrite of the ecosystem, no disruptive migration guide that forced a rethinking of everything. Instead, there has been a steady refinement of APIs and dependencies, each release smoothing a rough edge here and removing a surprise there.</p><p>The result isn’t flashy, yet it’s tangible. Writing integration tests against Elasticsearch now feels less like assembling a test harness and more like exercising a real system in miniature.</p><p>Sometimes progress announces itself loudly. Sometimes it arrives quietly, in the form of code that simply reads better and requires less explanation. In this case, it’s the latter, and for those of us who care about clean, reliable integration tests, that’s more than enough.</p><p>And what if we could do something similar with Kibana? Sounds appealing? Stay tuned!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-integration-tests</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-integration-tests</guid>
    <category><![CDATA[Java]]></category>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc31c3bf0d453d846/6a170e12cdacbfdf6a7d2a7e/3ae41b1f2876d2ad11c8e2b79bbf79955d6902aa-1440x840.png" length="0" type="image/png"/>
    <pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Hybrid search with Java: LangChain4j Elasticsearch integration]]></title>
    <description><![CDATA[Learn how to use hybrid search in LangChain4j via its Elasticsearch integrations, with a complete Java example.]]></description>
    <content:encoded><![CDATA[<p>In our <a href="https://www.elastic.co/search-labs/blog/langchain-elasticsearch-hybrid-search">previous article</a> on hybrid search with Elasticsearch in LangChain, we explained why hybrid search can help retrieve better results than simple vector search, along with how it works. We recommend reading that article first.</p><p>In addition to Python and JavaScript, the LangChain ecosystem also has a community-driven Java project called <a href="https://github.com/langchain4j/langchain4j">LangChain4j</a>, which will be the focus of this article, showing how powerful hybrid search can be by writing a complete application using LangChain4j, Elasticsearch, and Ollama.</p><h2>Setting up the environment</h2><h3>Running a local Elasticsearch instance</h3><p>Before running the examples, you'll need Elasticsearch running locally. The easiest way is using the <a href="https://github.com/elastic/start-local?tab=readme-ov-file"><code>start-local</code></a> script:</p>curl -fsSL https://elastic.co/start-local | sh<p>After starting, you'll have:</p><ul><li><p>Elasticsearch at http://localhost:9200.</p></li><li><p>Kibana at http://localhost:5601.</p></li></ul><p>Your API key is stored in the .env file (under the elastic-start-local folder) as <code>ES_LOCAL_API_KEY</code>.</p><p>&gt; <strong>Note: This script is for local testing only. Do not use it in production. For production installations, refer to the </strong><a href="https://www.elastic.co/downloads/elasticsearch"><strong>official documentation</strong></a><strong> for Elasticsearch.</strong></p><h3>Running a local Ollama instance</h3><p>You’ll also need to connect your application to an embedding model. Although you can choose between any provider supported by LangChain4j (check the <a href="https://docs.langchain4j.dev/integrations/language-models/">complete list</a>), for this example we’ll be using Ollama, which can be easily set up locally following the <a href="https://docs.ollama.com/quickstart">quickstart</a>.</p><h2>Let’s start coding</h2><p>The idea for the application is simple: Given a dataset of movies (taken from an IMDb dataset on <a href="https://www.kaggle.com/datasets/rajugc/imdb-movies-dataset-based-on-genre/versions/2?select=scifi.csv">Kaggle</a>), we want to be able to find movies whose descriptions are relevant to our queries. This demo uses a subset of the data, which has been cleaned. You can download the dataset used for this article from our <a href="https://github.com/elastic/hybrid-search-elastic-langchain4j">GitHub repo</a>, along with the full code for this demo.</p><h2>Step 1: Dependencies and environment</h2><p>Open your favorite integrated development environment (IDE), create a new blank project, preferably with a modern Java version (we’re using Java24) and a gradle/maven version to match (in our case, Gradle 9.0).</p><p>We only need three dependencies:</p>dependencies {
    implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.17.0")
    implementation("dev.langchain4j:langchain4j-elasticsearch:1.11.0-beta19")
    implementation("dev.langchain4j:langchain4j-ollama:1.11.0")
}<p>The first one is needed to ingest the data that we’ll embed and query; the other two are the necessary LangChain4j dependencies to connect and manage our Elasticsearch vector store and Ollama embedding model.</p><p>The best way to connect to the external services is to set up environment variables and set them at the start of our main function:</p>String elasticsearchServerUrl = System.getenv("ES_LOCAL_URL");
String elasticsearchApiKey = System.getenv("ES_LOCAL_API_KEY");

String ollamaUrl = System.getenv("ollama-url");
String ollamaModelName = System.getenv("model-name");<h2>Step 2: Ingesting the dataset</h2><p>Since the dataset is a CSV, we’ll be using <a href="https://github.com/FasterXML/jackson-dataformats-text">Jackson dataformat</a>’s <code>jackson-dataformat-csv</code> to easily read the data and map it to a Java class, defined as:</p>public record Movie(
    String movie_id,
    String movie_name,
    Integer year,
    String genre,
    String description,
    String director
) {
}<p>Now we can create an instance of <code>CsvSchema</code> mapping the CSV structure and read the file into an iterator:</p>CsvSchema schema = CsvSchema.builder()                    
    .addColumn("movie_id") // same order as in the csv    
    .addColumn("movie_name")                              
    .addColumn("year")                                    
    .addColumn("genre")                                   
    .addColumn("description")                             
    .addColumn("director")                                
    .setColumnSeparator(',')                              
    .setSkipFirstDataRow(true)                            
    .build();                                             
                                                          
CsvMapper csvMapper = new CsvMapper();                    
                                                          
File initialFile = new File("src/main/resources/scifi_1000.csv");
InputStream csvContentStream = new FileInputStream(initialFile);
                                                          
MappingIterator&lt;Movie&gt; it = csvMapper                     
    .readerFor(Movie.class)                               
    .with(schema)                                         
    .readValues(new InputStreamReader(csvContentStream)); <p>Each row needs to be embedded first, and then both the embedded content and the text representation will be ingested by Elasticsearch.</p><p>Let’s start by creating an instance of the Ollama embedding model class:</p>EmbeddingModel embeddingModel = OllamaEmbeddingModel.builder()
    .baseUrl(ollamaUrl)
    .modelName(ollamaModelName)
    .build(); <p>And then the Elasticsearch vector store, which needs an instance of the Elasticsearch Java RestClient:</p>RestClient restClient = RestClient
    .builder(HttpHost.create(elasticsearchServerUrl))
    .setDefaultHeaders(new Header[]{
        new BasicHeader("Authorization", "ApiKey " + elasticsearchApiKey)
    })
    .build(); 

EmbeddingStore&lt;TextSegment&gt; embeddingStore = ElasticsearchEmbeddingStore.builder()
    .restClient(restClient)
    .build(); <p>For the ingestion loop, the LangChain4j library requires the data to be split in two lists for ingestion, one for the vector representation and one for the original text, so we’ll set up two lists which will be filled by the loop:</p>List&lt;Embedding&gt; embeddings = new ArrayList&lt;&gt;();
List&lt;TextSegment&gt; embedded = new ArrayList&lt;&gt;();<p>Where <code>Embedding</code> and <code>TextSegment</code> are both library specific classes.</p><p>We’ll iterate on the movie dataset iterator, use the embedding model to retrieve the vector representation for each movie information (a text representation of all the fields merged), and add the name separately as metadata so that the result will be easier to read.</p>boolean hasNext = true;

while (hasNext) {
    try {
        Movie movie = it.nextValue();
        String text = movie.toString();

        Embedding embedding = embeddingModel.embed(text).content();
        embeddings.add(embedding);

        Metadata metadata = new Metadata();
        metadata.put("movie_name", movie.movie_name());
        embedded.add(new TextSegment(text, metadata));

        hasNext = it.hasNextValue();
    } catch (JsonParseException | InvalidFormatException e) {
        // ignore malformed data
    }
}<p>Finally, the vector list and text list are passed to the vector store method <code>addAll()</code>, which will handle asynchronously sending the data to the vector store:</p>embeddingStore.addAll(embeddings, embedded);<h2>Step 3: Querying</h2><p>Our goal is to find movies with time loops in the plot, so our prompt will be:</p>String query = "Find movies where the main character is stuck in a time loop and reliving the same day.";<p>Let’s try a simple vector search first, by creating a content retriever with a <a href="https://www.elastic.co/docs/solutions/search/vector/knn">k-nearest neighbor (kNN) query</a> default configuration and then running the query and printing the results:</p>ElasticsearchContentRetriever contentRetrieverVector = ElasticsearchContentRetriever.builder()
                .restClient(restClient)
                .configuration(ElasticsearchConfigurationKnn.builder().build())
                .maxResults(5)
                .embeddingModel(embeddingModel)
                .build();

List&lt;Content&gt; vectorSearchResult = contentRetrieverVector.retrieve(Query.from(query));

System.out.println("Vector search results:");
vectorSearchResult.forEach(v -&gt; System.out.println(v.textSegment().metadata().getString(
                "movie_name")));<p>This outputs:</p>Vector search results:
The Witch: Part 1 - The Subversion
Divinity
The Maze Runner
Spider-Man
Spider-Man: Into the Spider-Verse<p>Now let’s see how hybrid search performs:</p>ElasticsearchContentRetriever contentRetrieverHybrid = ElasticsearchContentRetriever.builder()
    .restClient(restClient)
    .configuration(ElasticsearchConfigurationHybrid.builder().build())
    .maxResults(5)
    .embeddingModel(embeddingModel)
    .build();

List&lt;Content&gt; hybridSearchResult = contentRetrieverHybrid.retrieve(Query.from(query));

System.out.println("Hybrid search results:");
hybridSearchResult.forEach(v -&gt; System.out.println(v.textSegment().metadata().getString(
            "movie_name")));Hybrid search results:
Edge of Tomorrow
The Witch: Part 1 - The Subversion
Boss Level
Divinity
The Maze Runner<h2>Why these results?</h2><p>This query (“time loop / reliving the same day”) is a great case where hybrid search tends to shine because the dataset contains literal phrases that BM25 can match and vectors can still capture meaning.</p><ul><li><p>Vector-only (kNN) embeds the query and tries to find semantically similar plots. Using a broad sci‑fi dataset, this can drift into “trapped / altered reality / memory loss / high-stakes sci‑fi” even when there’s no time-loop concept. That’s why results like “The Witch: Part 1 – The Subversion” (amnesia) and “The Maze Runner” (trapped / escape) can appear.</p></li><li><p>Hybrid (BM25 + kNN + reciprocal rank fusion [RRF]) rewards documents that match keywords and meaning. Movies whose descriptions explicitly mention “time loop” or “relive the same day” get a strong lexical boost, so titles like “Edge of Tomorrow” (relive the same day over and over again…) and “Boss Level” (trapped in a time loop that constantly repeats the day…) rise to the top.</p></li></ul><p>Hybrid search doesn’t guarantee that every result is perfect; it balances lexical and semantic signals, so you may still see some non-time-loop sci‑fi in the tail of the top‑k.</p><p>The main takeaway is that hybrid search helps anchor semantic retrieval with exact textual evidence when the dataset contains those keywords. Check the <a href="https://www.elastic.co/search-labs/blog/langchain-elasticsearch-hybrid-search">previous article</a> for more information on how hybrid search works.</p><h2>Full code example</h2><p>You can find the full demo code on <a href="https://github.com/elastic/hybrid-search-elastic-langchain4j">GitHub</a>.</p><h2>Conclusion</h2><p>In this article, we demonstrated how to use hybrid search in LangChain4j through its Elasticsearch integrations, with a complete Java example. This article is an extension of a <a href="https://www.elastic.co/search-labs/blog/langchain-elasticsearch-hybrid-search">previous article</a>, which presents the LangChain integrations for Python and JavaScript and introduces and explains hybrid search. We’re planning to continue our collaboration with LangChain4j in the future by contributing to the embedding models with our Elasticsearch <a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">Inference API</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/langchain4j-elasticsearch-hybrid-search</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/langchain4j-elasticsearch-hybrid-search</guid>
    <category><![CDATA[Hybrid Search]]></category>
    <category><![CDATA[Integrations]]></category>
    <category><![CDATA[Java]]></category>
    <dc:creator><![CDATA[Laura Trotta]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt63218799e944e16d/6a1710ef6f7f04ee68914952/93d8e0d84fb4cfbf5e51df85df7ec2e600d9dcc7-1088x607.png" length="0" type="image/png"/>
    <pubDate>Wed, 11 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>