I remember the first time a product manager asked me, “Why does our search feel so slow? Why can’t users find what they want?” I was staring at a PostgreSQL table with millions of rows, running LIKE '%keyword%' queries. The database was gasping. The answer was clear: we needed a dedicated search engine. That’s when I started looking at Elasticsearch and its integration with Spring Boot.
If you’ve built any enterprise application, you know the pain. Traditional databases are great at transactions, but terrible at full-text search. They don’t handle fuzzy matches, typo tolerance, relevance scoring, or faceted filters well. Elasticsearch changes that. And Spring Data Elasticsearch lets you talk to it the same way you talk to JPA repositories. No magic, just clean code.
Let me walk you through what I learned, and how you can do it too.
First, you need to add the right dependency. With Spring Boot, it’s trivial. In your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
That’s it. Spring Boot auto-configures an ElasticsearchRestTemplate and connects to a local cluster at localhost:9200. If you’re running Elasticsearch via Docker, you can use the official image:
docker run -d --name elasticsearch -p 9200:9200 -e "discovery.type=single-node" elasticsearch:8.10.2
Now, define a document entity. Think of it like a JPA entity, but for search.
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "standard")
private String name;
@Field(type = FieldType.Text)
private String description;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Double)
private Double price;
// constructors, getters, setters
}
Notice the @Document annotation with the index name. The @Field annotations control how Elasticsearch indexes and stores data. Text fields are analyzed for full‑text search. Keyword fields are used for exact matches, aggregations, and sorting.
What is the difference between Text and Keyword? I had to learn this the hard way when my search returned no results because I was using Keyword for a product name. Text gets broken into tokens for matching; Keyword stays as a single block and is great for filters like “category” or “status”.
Now, create a repository interface. This is where Spring Data Elasticsearch shines.
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
List<Product> findByNameContainingIgnoreCase(String name);
List<Product> findByCategoryAndPriceBetween(String category, Double min, Double max);
List<Product> findByNameOrDescriptionContaining(String keyword);
}
You don’t write a single line of implementation. The method names follow conventions similar to JPA. Behind the scenes, Spring Data Elasticsearch builds the appropriate Elasticsearch queries.
But what if you need something more complex? A fuzzy search that handles typos? Then you use @Query annotation or the native query builder. Here’s an example with a custom query:
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
@Query("{\"fuzzy\": {\"name\": {\"value\": \"?0\", \"fuzziness\": \"AUTO\"}}}")
List<Product> fuzzySearchByName(String name);
}
The JSON query is embedded as a string. Yes, it looks ugly, but it works. For more dynamic scenarios, I prefer using the ElasticsearchOperations bean (or ElasticsearchRestTemplate). Here’s a personal touch from my project:
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import java.util.List;
import static org.elasticsearch.index.query.QueryBuilders.*;
@Service
public class ProductSearchService {
private final ElasticsearchOperations operations;
public ProductSearchService(ElasticsearchOperations operations) {
this.operations = operations;
}
public List<Product> search(String keyword, Double maxPrice) {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery()
.must(multiMatchQuery(keyword, "name", "description"))
.filter(rangeQuery("price").lte(maxPrice)))
.withPageable(PageRequest.of(0, 10))
.build();
return operations.search(query, Product.class)
.stream()
.map(SearchHit::getContent)
.toList();
}
}
This query matches the keyword in both name and description fields, but only returns results with price less than or equal to the max price. The rangeQuery is a filter that does not affect relevance scoring. I use this in my e‑commerce search endpoint where users want both text relevance and price constraints.
Now, let’s talk about indexing. When you save a product using repository.save(product), Spring Data Elasticsearch automatically indexes it. But what if you have existing data in a relational database? You need to bulk index. Here’s a snippet I wrote for a batch job:
@Component
public class DataIndexer {
private final ProductRepository repository;
private final JdbcTemplate jdbcTemplate;
public DataIndexer(ProductRepository repository, JdbcTemplate jdbcTemplate) {
this.repository = repository;
this.jdbcTemplate = jdbcTemplate;
}
@PostConstruct
public void indexProducts() {
List<Product> products = jdbcTemplate.query("SELECT * FROM products",
(rs, rowNum) -> new Product(
rs.getString("id"),
rs.getString("name"),
rs.getString("description"),
rs.getString("category"),
rs.getDouble("price")
)
);
repository.saveAll(products);
}
}
This runs on startup, but in production you’d schedule it or use a listener. I have seen teams forget to index after database inserts – their search stayed empty. Always sync.
What about aggregations? Suppose you want a count of products per category, with average price. Elasticsearch aggregations are powerful. Using Spring Data Elasticsearch:
public Map<String, Long> countByCategory() {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.addAggregation(AggregationBuilders.terms("categories").field("category"))
.build();
return operations.aggregate(query, Product.class, Aggregation.class)
.aggregations()
.asMap();
}
But aggregations are complex. I once spent hours debugging because my field type was Text instead of Keyword. Aggregations work only on Keyword or numeric fields. Learn from my mistake.
Now, here’s a question for you: Have you ever tried to implement autocomplete or “did you mean?” suggestions? Elasticsearch provides completion suggesters. In Spring Boot, you can annotate a field with @CompletionField and then use the SuggestBuilder. But I won’t go into that now – just know it’s possible and works beautifully.
Why should you care about this integration? Because it scales. Elasticsearch can handle hundreds of millions of documents. Spring Boot gives you a clean, testable service layer. Together, they let you build search that feels instant. Users stay engaged. Conversion rates improve.
I’ve used this combination in three enterprise projects now, and each time the team was surprised by how little code was needed. No JDBC, no raw JSON, no manual HTTP calls. Just repositories and templates.
Before I wrap up, I need to mention one more thing: error handling. Elasticsearch can be down. Your app shouldn’t crash. Wrap your search calls in try-catch and handle ElasticsearchStatusException gracefully. Return an empty list or a fallback message. I always add a circuit breaker pattern with resilience4j when indexing large volumes.
Now, let’s talk about tuning. By default, Spring Data Elasticsearch uses a transport client? No – since version 4.0, it uses the official Elasticsearch REST client. You can configure connection properties in application.yml:
spring:
elasticsearch:
uris: http://localhost:9200
connection-timeout: 5s
socket-timeout: 60s
For production, add multiple nodes, enable authentication, and use SSL. But keep it simple for local development.
I hope this article gave you a clear, practical understanding. If you found it useful, hit that like button, leave a comment about your own experience with Elasticsearch, and share it with a colleague who’s still using LIKE '%search%'. Trust me, they need this. And if you have questions, ask them below – I read every comment.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva