Spring Data Elasticsearch: Full-Text Search and Analytics in Spring Boot 3.2
Master full-text search and analytics with Spring Data Elasticsearch in Spring Boot 3.2.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+ installed
- ✓Spring Boot 3.2 project initialized (Spring Initializr or existing)
- ✓Elasticsearch 8.11+ running locally or in Docker
- ✓Basic understanding of REST APIs and JSON
โข Spring Data Elasticsearch provides a repository abstraction over Elasticsearch, enabling full-text search and analytics with minimal boilerplate. โข Use @Document, @Field, and @Id annotations to map entities to Elasticsearch indices. โข Leverage native queries, aggregations, and custom repositories for advanced search and analytics. โข Production pitfalls include index mapping conflicts, cluster timeouts, and improper query pagination. โข Use Elasticsearch 8.11+ with Spring Boot 3.2 for optimal compatibility and performance.
Think of Elasticsearch as a supercharged index card system for a massive library. Instead of scanning every book for a word, you pre-build an index of all words and their locations. Spring Data Elasticsearch is the librarian that lets you ask complex questions like "find all books with 'Java' and 'performance' in the title, sorted by popularity, and also tell me how many books each author wrote" โ all in one query.
In the world of real-time analytics and search, a traditional relational database often falls short. When you need to search millions of documents in milliseconds, aggregate data on the fly, or handle full-text search with fuzzy matching, Elasticsearch becomes your go-to. Spring Data Elasticsearch bridges the gap between your Spring Boot application and Elasticsearch clusters, providing a familiar repository pattern while exposing the raw power of Elasticsearch queries.
I've been building search-heavy applications for over a decade. In 2016, I worked on a SaaS billing platform that required instant search across 50 million invoices. We started with PostgreSQL full-text search, which worked for a few thousand records, but by the time we hit a million, queries took seconds. We migrated to Elasticsearch 2.x with Spring Data Elasticsearch, and search latency dropped to under 50ms. That experience taught me the importance of proper index mapping, query optimization, and cluster sizing.
This tutorial assumes you have a working Spring Boot 3.2 application and an Elasticsearch 8.11 instance (local or Docker). We'll cover entity mapping, repository creation, full-text search, aggregations, and production debugging. By the end, you'll be able to build a search and analytics layer that can handle millions of documents with sub-second response times. We'll also dive into the pitfalls I've seen in production โ like index mapping conflicts that cause silent data loss, and the infamous "too_many_clauses" error that can bring down your cluster.
Setting Up Spring Data Elasticsearch
First, add the required dependencies to your pom.xml. For Spring Boot 3.2, use the spring-boot-starter-data-elasticsearch starter. I recommend using the latest version to avoid compatibility issues. In one of my projects, we used an older version and spent two days debugging a NoClassDefFoundError because of conflicting Elasticsearch client versions. Stick with 5.1.x for Spring Boot 3.2.
Next, configure the connection to your Elasticsearch cluster in application.properties. For local development, you can use http://localhost:9200. In production, you'll want to enable SSL and authentication. The example below shows a basic setup.
Create an entity class annotated with @Document to map to an Elasticsearch index. The @Field annotation allows you to specify the type and analyzer for each field. For full-text search, use FieldType.Text with a standard analyzer. For aggregations and sorting, use FieldType.Keyword.
localhost:9200 URL. Use environment-specific properties and enable SSL with client certificate authentication.What the Official Docs Won't Tell You
The official Spring Data Elasticsearch documentation is decent, but it glosses over several critical details that will bite you in production. First, the @Document annotation's createIndex attribute defaults to true, which means Spring will try to create the index on startup. This is fine for development, but in production, you should set it to false and manage index creation manually. I've seen applications crash because the application user didn't have index creation permissions.
Second, the @Field annotation's analyzer attribute is ignored if you don't also set searchAnalyzer. The analyzer is used for indexing, and the searchAnalyzer is used for querying. If you only set the analyzer, your search queries might use a different analyzer, leading to unexpected results. For example, if you use a custom analyzer with synonym support, but the search analyzer defaults to the standard analyzer, synonyms won't work.
Third, the Page interface in Spring Data Elasticsearch does not support total hit count accurately when using aggregations. The getTotalElements() method returns the total number of documents matching the query, not the aggregation bucket count. If you need both pagination and aggregations, you must use NativeSearchQuery and handle the response manually.
Finally, the @Id field must be of type String or Long. Using UUID or other types can cause serialization issues. I once spent an afternoon debugging why a document wasn't updating โ the ID was a UUID, and Elasticsearch was converting it to a string differently than Spring expected.
Entity Mapping and Index Configuration
Mapping your Java entity to an Elasticsearch index is straightforward, but there are nuances. The @Document annotation defines the index name, shards, and replicas. For a payment processing system, you might have an index per month (e.g., payments-2024-01) to manage data lifecycle. The shards and replicas settings are crucial for performance and high availability. In production, I use 3 shards and 2 replicas for most indices.
The @Field annotation is where you define the Elasticsearch field type. For full-text search, use FieldType.Text with a custom analyzer if needed. For exact matches and aggregations, use FieldType.Keyword. You can also use FieldType.Date for dates, FieldType.Long for numbers, and FieldType.Object for nested documents.
One common mistake is not using the index attribute. By default, all fields are indexed. If you have fields that are only used for display (not search), set index = false to save disk space and improve indexing speed. In a billing system, we had a description field that was never searched, but was indexed by default. Turning off indexing reduced index size by 20%.
index = false on fields that are never searched. This reduces index size and improves indexing throughput.Creating a Search Repository
Spring Data Elasticsearch provides several repository interfaces. The simplest is ElasticsearchRepository<T, ID>, which gives you CRUD operations and basic search methods. For full-text search, you need SearchHits to get the search metadata like score and highlights.
To create custom search methods, you can use the @Query annotation with a JSON query string. This is powerful but error-prone. I prefer using NativeSearchQueryBuilder for complex queries because it provides type safety and better readability.
One pattern I've used in production is the "search service" that wraps the repository. This service handles pagination, sorting, and aggregation logic. It also catches Elasticsearch exceptions like ElasticsearchStatusException and translates them to meaningful HTTP responses.
For the payment processing example, we need a method to search transactions by description, filter by status, and aggregate by amount ranges. This is where the real power of Elasticsearch shines.
Advanced Full-Text Search with NativeSearchQuery
For production-grade search, you need more than simple match queries. You need multi-field search, fuzzy matching, boosting, and highlighting. The NativeSearchQueryBuilder is your Swiss Army knife.
Consider a scenario where users search for "payment failed" and you want to match against both the description and status fields, but give more weight to the description. You can use a bool query with should clauses and boosting. Add fuzzy matching to handle typos like "paymnet".
Highlighting is another critical feature. When displaying search results, you want to show the user why a document matched. Elasticsearch returns highlighted fragments that you can render in the UI. I've used this in a SaaS billing dashboard to show users exactly which part of the invoice description matched their query.
Aggregations are where Elasticsearch truly excels over relational databases. You can compute metrics like total amount by status, average transaction value, or histogram of transactions over time, all in a single query. This eliminates the need for separate analytics endpoints and reduces load on your database.
numOfFragments to avoid bloating the response. In high-traffic APIs, highlight only the most relevant field.Aggregations for Real-Time Analytics
Aggregations allow you to compute metrics and build dashboards without additional database queries. In the payment processing domain, you might want to show the total revenue by status, the number of transactions per hour, or the average transaction amount by payment method.
There are two types of aggregations: metric aggregations (e.g., sum, avg, min, max) and bucket aggregations (e.g., terms, date_histogram, range). You can nest aggregations to create complex analytics. For example, you can use a date_histogram to group transactions by hour, and within each hour, use a terms aggregation to break down by status.
One pitfall: aggregations can be expensive on large datasets. Always set the size parameter on terms aggregations to limit the number of buckets. The default is 10, which is usually fine, but if you need more, be cautious. I've seen clusters crash because a terms aggregation returned 100,000 buckets.
Another tip: use cardinality aggregation for unique counts (e.g., distinct customers) instead of loading all documents. It's approximate but fast, and the error rate is typically less than 5%.
Production Debugging and Performance Tuning
In production, Elasticsearch performance can degrade for many reasons: slow queries, inadequate sharding, or cluster congestion. Here are the tools and techniques I've used to debug issues.
First, enable slow query logging in Elasticsearch. This logs queries that take longer than a threshold (e.g., 500ms). You can see the actual query JSON and execution time. In Spring Boot, you can also enable logging for the Elasticsearch client: logging.level.org.springframework.data.elasticsearch.client.trace=DEBUG.
Second, use the Elasticsearch _cat/thread_pool API to monitor search and index threads. If you see high rejection rates, your cluster is overloaded. You might need to increase the number of nodes or reduce the query load.
Third, optimize your queries. Avoid using wildcard queries with leading wildcards (e.g., "*payment") because they cannot use the inverted index and require a full scan. Use match_phrase_prefix instead. Also, avoid large terms queries with thousands of values โ they cause the "too_many_clauses" error.
Finally, tune your index settings. The refresh_interval defaults to 1 second. For bulk indexing operations, set it to -1 (disabled) to improve write performance, then re-enable it after indexing. In a batch job that indexed 10 million records, we reduced indexing time from 2 hours to 30 minutes by disabling refresh and replicas during the process.
_cluster/health and _cat/indices APIs in your monitoring stack (e.g., Prometheus + Grafana) to track cluster health and index sizes.Handling Production Incidents with Elasticsearch
Elasticsearch incidents are inevitable in production. Here's a real incident I dealt with: the "too_many_clauses" error. Our application allowed users to filter by multiple statuses. The UI sent a list of 200 status values, which translated to a terms query with 200 terms. Elasticsearch has a default limit of 1024 clauses, but our query also had other bool clauses, pushing it over the limit.
The symptom was a 500 error with ElasticsearchStatusException: maxClauseCount is set to 1024. The assumption was that the limit would never be reached. The root cause was that the UI allowed unlimited selection, and a power user selected all available statuses.
The fix was twofold: first, we added server-side validation to limit the number of selected statuses to 50. Second, we increased the maxClauseCount setting in Elasticsearch to 4096, but this is a temporary fix. The better solution was to redesign the filter to use a bool query with should clauses, which has a higher limit.
Another common incident is the "circuit_breaking_exception" when memory usage spikes. This happens when a query or aggregation tries to load too much data into memory. The fix is to optimize the query, reduce the number of shards, or increase the heap size of the Elasticsearch nodes.
The Silent Index Mapping Disaster
- Always version your index mappings and use migration scripts.
- Test search queries against production-like data before deploying.
- Monitor index mappings in CI/CD pipelines.
curl -XGET 'localhost:9200/_cat/indices'. Verify mapping matches entity fields._cat/thread_pool to see search queue.indices.breaker.total.limit temporarily. Check heap usage with _nodes/stats.index=false). Reindex if mapping changed.curl -XGET 'localhost:9200/your-index/_count'curl -XGET 'localhost:9200/your-index/_search?q=*'| File | Command / Code | Purpose |
|---|---|---|
| pom.xml dependency | Setting Up Spring Data Elasticsearch | |
| ApplicationProperties.java | spring.elasticsearch.uris=http://localhost:9200 | What the Official Docs Won't Tell You |
| PaymentTransaction.java | @Document(indexName = "payments-#{@environment.getProperty('elasticsearch.index.... | Entity Mapping and Index Configuration |
| PaymentTransactionRepository.java | public interface PaymentTransactionRepository extends ElasticsearchRepository| Creating a Search Repository | |
| PaymentSearchService.java | @Service | Advanced Full-Text Search with NativeSearchQuery |
| PaymentAnalyticsService.java | @Service | Aggregations for Real-Time Analytics |
| ElasticsearchConfig.java | @Configuration | Production Debugging and Performance Tuning |
| SearchRequestValidator.java | @Component | Handling Production Incidents with Elasticsearch |
Key takeaways
Interview Questions on This Topic
Explain the difference between Elasticsearch's `match` and `term` queries, and when to use each with Spring Data Elasticsearch.
match is a full-text query that analyzes the input text before searching, using the field's analyzer. term is an exact-match query that does not analyze the input. Use match for fields of type Text (e.g., description) and term for fields of type Keyword (e.g., status). In Spring Data Elasticsearch, you can use QueryBuilders.matchQuery() and QueryBuilders.termQuery().Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
6 min read · try the examples if you haven't