Migrating from the High Level REST Client to the Elasticsearch Java API Client ChatGPT Image Dec 9 2025 10 48 58 AM

Migrating from the High Level REST Client to the Elasticsearch Java API Client

Applications built with Java and using Elasticsearch traditionally use the High Level REST Client (HLRC). It was the go-to choice for years, gave you type-safe requests and was the client supported by Elasticearch themself.

Well, that party’s over. Elastic deprecated the HLRC in 7.15, then completely froze it in 7.17. No more updates. And the kicker is: it is not supported by Elasticsearch 8.x and Elasticsearch 7.17.x itself reaches its End of Support in January 2026.

So if you want to upgrade your cluster past 7.x, or you just want your app to have a future, you need to migrate to the new Elasticsearch Java API Client.

2. The New Java API Client

The replacement is called the Elasticsearch Java API Client (elasticsearch-java). Elastic released it in 7.15, and it’s fully supported in 8.x and beyond.

The big difference? This client is auto-generated from Elasticsearch’s REST API spec. That means it stays perfectly in sync with whatever Elasticsearch can do, no more waiting for someone to manually update client methods when new features come out.

They also built it with modern Java in mind. You get builder syntax that, proper type safety with POJOs, and it works with sync, async, and reactive patterns. Plus, it’s the only official Java client they’re maintaining going forward.


3. How different is it really?

Pretty different, unfortunately. This isn’t a simple dependency swap.

The main class changed from RestHighLevelClient to ElasticsearchClient, but they also split out transport handling into RestClientTransport. Your Maven dependency changes from elasticsearch-rest-high-level-client to elasticsearch-java.

The biggest philosophical change is how they handle API coverage. The old client was manually maintained, so sometimes new Elasticsearch features would take months to show up. The new one is generated directly from the REST API spec, so it’s always current.

AspectHLRC (old)Java API Client (new)
Dependencyelasticsearch-rest-high-level-clientelasticsearch-java
TransportRestHighLevelClientElasticsearchClient + RestClientTransport
API CoverageManually maintainedGenerated from REST API spec → always up-to-date
Cluster Support5.x–7.x7.15+ (best with 8.x)
Future SupportDeprecated, frozen in 7.17Actively developed


4. Migration

4.1 Client Setup

HLRC (old):

RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(
        new HttpHost("localhost", 9200, "http")
    )
);

Java API Client (new):

RestClient restClient = RestClient.builder(
    new HttpHost("localhost", 9200)
).build();

RestClientTransport transport = new RestClientTransport(
    restClient, new JacksonJsonpMapper()
);

ElasticsearchClient client = new ElasticsearchClient(transport);

4.2 Document Indexing

HLRC :

IndexRequest request = new IndexRequest("products")
    .id("1")
    .source(XContentType.JSON,
        "name", "Laptop",
        "price", 999.99,
        "category", "electronics"
    );

IndexResponse response = client.index(request, RequestOptions.DEFAULT);

Java API Client:

Product product = new Product("Laptop", 999.99, "electronics");

IndexResponse response = client.index(IndexRequest.of(i -> i
    .index("products")
    .id("1")
    .document(product)
));

// Or with builder pattern:
IndexRequest.Builder<Product> indexReqBuilder = new IndexRequest.Builder<>();
IndexRequest<Product> indexRequest = indexReqBuilder
    .index("products")
    .id("1")
    .document(product)
    .build();

4.3 Document Retrieval

HLRC:

GetRequest getRequest = new GetRequest("products", "1");
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);

if (getResponse.isExists()) {
    String sourceAsString = getResponse.getSourceAsString();
    Map<String, Object> sourceAsMap = getResponse.getSourceAsMap();
}

Java API Client:

GetResponse<Product> response = client.get(GetRequest.of(g -> g
    .index("products")
    .id("1")
), Product.class);

if (response.found()) {
    Product product = response.source();
    // Direct POJO access - no manual parsing needed
}

4.4 Search Operations

HLRC:

SearchRequest searchRequest = new SearchRequest("products");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("name", "laptop"));
searchSourceBuilder.from(0);
searchSourceBuilder.size(10);
searchRequest.source(searchSourceBuilder);

SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

SearchHit[] searchHits = searchResponse.getHits().getHits();
for (SearchHit hit : searchHits) {
    String sourceAsString = hit.getSourceAsString();
    // Manual parsing required
}

Java API Client:

SearchResponse<Product> response = client.search(SearchRequest.of(s -> s
    .index("products")
    .query(q -> q
        .match(t -> t
            .field("name")
            .query("laptop")
        )
    )
    .from(0)
    .size(10)
), Product.class);

List<Hit<Product>> hits = response.hits().hits();
for (Hit<Product> hit : hits) {
    Product product = hit.source();
    // Direct POJO access
}

4.5 Bulk Operations

HLRC:

BulkRequest request = new BulkRequest();
request.add(new IndexRequest("products")
    .id("1")
    .source(XContentType.JSON, "name", "Product 1"));
request.add(new IndexRequest("products")
    .id("2")
    .source(XContentType.JSON, "name", "Product 2"));

BulkResponse bulkResponse = client.bulk(request, RequestOptions.DEFAULT);

Java API Client:

List<BulkOperation> bulkOperations = Arrays.asList(
    BulkOperation.of(o -> o
        .index(IndexOperation.of(i -> i
            .index("products")
            .id("1")
            .document(new Product("Product 1", 100.0, "electronics"))
        ))
    ),
    BulkOperation.of(o -> o
        .index(IndexOperation.of(i -> i
            .index("products")
            .id("2")
            .document(new Product("Product 2", 200.0, "electronics"))
        ))
    )
);

BulkResponse response = client.bulk(BulkRequest.of(b -> b
    .operations(bulkOperations)
));

4.6 Aggregations

HLRC:

SearchRequest searchRequest = new SearchRequest("products");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(
    AggregationBuilders.terms("categories").field("category.keyword")
);
searchRequest.source(searchSourceBuilder);

SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
Terms categoriesAgg = searchResponse.getAggregations().get("categories");

Java API Client:

SearchResponse<Product> response = client.search(SearchRequest.of(s -> s
    .index("products")
    .size(0)
    .aggregations("categories", a -> a
        .terms(t -> t
            .field("category.keyword")
        )
    )
), Product.class);

StringTermsAggregate categories = response.aggregations()
    .get("categories")
    .sterms();

4.7 Exception Handling

HLRC (old):

try {
    GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
} catch (ElasticsearchException e) {
    if (e.status() == RestStatus.NOT_FOUND) {
        // Handle document not found
    }
} catch (IOException e) {
    // Handle connection issues
}

Java API Client:

try {
    GetResponse<Product> response = client.get(getRequest, Product.class);
} catch (ElasticsearchException e) {
    if (e.response().status() == 404) {
        // Handle document not found
    }
} catch (IOException e) {
    // Handle connection issues
}

4.8 Creating POJOs for Type Safety

One of the biggest advantages of the new client is proper type safety. Create POJOs for your documents:

public class Product {
    private String name;
    private Double price;
    private String category;
    
    // Constructors
    public Product() {}
    
    public Product(String name, Double price, String category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }
    
    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
    
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
}

4.9 Configuration and Authentication

HLRC:

final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
    new UsernamePasswordCredentials("username", "password"));

RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(new HttpHost("localhost", 9200))
        .setHttpClientConfigCallback(httpClientBuilder ->
            httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider))
);

Java API Client:

final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
    new UsernamePasswordCredentials("username", "password"));

RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200))
    .setHttpClientConfigCallback(httpClientBuilder ->
        httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider))
    .build();

RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);

4.10 Migration Strategy

  1. Add new dependency alongside the old one temporarily
  2. Create wrapper methods that handle both clients during transition
  3. Migrate operation by operation, testing thoroughly
  4. Remove HLRC dependency once migration is complete

Example wrapper during migration:

public class ElasticsearchService {
    private final RestHighLevelClient hlrcClient;  // Remove after migration
    private final ElasticsearchClient newClient;
    private final boolean useNewClient;
    
    public Product getProduct(String id) throws IOException {
        if (useNewClient) {
            return getProductNew(id);
        } else {
            return getProductOld(id);
        }
    }
    
    private Product getProductNew(String id) throws IOException {
        GetResponse<Product> response = newClient.get(GetRequest.of(g -> g
            .index("products")
            .id(id)
        ), Product.class);
        
        return response.found() ? response.source() : null;
    }
    
    private Product getProductOld(String id) throws IOException {
        GetRequest request = new GetRequest("products", id);
        GetResponse response = hlrcClient.get(request, RequestOptions.DEFAULT);
        
        if (response.isExists()) {
            // Manual JSON parsing to Product object
            ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(response.getSourceAsString(), Product.class);
        }
        return null;
    }
}

5. Risks and Considerations

  • The migration is not a simple drop-in replacement, refactoring is required.
  • Some APIs may differ in structure and require adjusting how request objects are built.
  • If you have custom serialization logic, validate that it works with the new client’s Jackson-based transport.

6. When to do this

Start now while you’re still on Elasticsearch 7.17. That way you can run both clients side-by-side during the migration, which makes testing less scary.

Best is to finish before January 2026 when 7.17 goes end-of-life, touhgh maintenance ended April 15, 2025 already. The good news is once you’re on the new client, future Elasticsearch upgrades will be smooth sailing – no more major client rewrites.


Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.