In this tutorial, we will implement Spring Boot Elasticsearch Vector Search using:
- Full-text search (BM25)
- Fuzzy search
- kNN vector search
- Semantic search (kNN + BM25 + Fuzzy)
We assume that embeddings are already stored in Elasticsearch during document ingestion using the embeddings field and model id all-MiniLM-L6-v2.
You can refer this elsticsearch doc for storing embeddings in Elasticsearch.
You can also generate embeddings using:
- OpenAI embedding models (text-embedding-3-small)
- Sentence Transformers
- HuggingFace models
Maven Dependencies
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.2.5</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>9.2.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
We use:
- elasticsearch-java -> Official high-level Java client
- rest-client -> Low-level HTTP transport
- Lombok -> Boilerplate reduction
application.yml Configuration
spring:
application:
name: elastic-knn
devglan:
elastic:
url: http://127.0.0.1:9200
apiKey: YOUR_BASE64_ENCODED_API_KEY
similarityThreshold: "60%"
minScore: 0.91
modelId: all-MiniLM-L6-v2
indices:
- my-documents
Configuration Explanation
- modelId -> Embedding model deployed in Elasticsearch
- minScore -> Filters low-quality vector matches
- similarityThreshold -> Minimum match percentage for filters
- indices -> Target index list
ElasticsearchClient Configuration
@Configuration
@RequiredArgsConstructor
public class ElasticSearchConfig {
private final ElasticProperties elasticProperties;
@Bean
public ElasticsearchClient elasticsearchClient() {
Header[] defaultHeaders = new Header[]{
new BasicHeader(HttpHeaders.AUTHORIZATION,
"ApiKey " + elasticProperties.getApiKey())
};
RestClient restClient = RestClient.builder(
HttpHost.create(elasticProperties.getUrl())
)
.setDefaultHeaders(defaultHeaders)
.build();
ElasticsearchTransport transport =
new RestClientTransport(restClient, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}
}
This creates a reusable, thread-safe Elasticsearch client.
Full Text Search (BM25)
private Function<Query.Builder, ObjectBuilder<Query>> fullTextQuery(String text) {
return q -> q.multiMatch(m -> m
.query(text)
.fields("title^3", "body^2", "text^2")
.type(TextQueryType.MostFields)
);
}
How It Works
- Uses multi_match
- BM25 scoring algorithm
- Boosting applied: title > body > text
MostFieldscombines scores across fields
Best for: Keyword-based search.
Fuzzy Search
private Function<Query.Builder, ObjectBuilder<Query>> fuzzyTextQuery(String text) {
return q -> q.multiMatch(m -> m
.query(text)
.fields("title", "body", "text")
.fuzziness("1")
.prefixLength(3)
.analyzer("standard")
.type(TextQueryType.MostFields)
);
}
How It Works
- fuzziness("1") -> Allows one edit distance
- prefixLength(3) -> First 3 characters must match
- Handles typos and spelling mistakes
Best for: User-generated search queries.
Spring Boot Elasticsearch kNN Vector Search (Pure Semantic Search)
This is NOT hybrid search.
It is: kNN semantic ranking + filter(bool(must: BM25 + fuzzy))
public List<SearchResult> similaritySearch(String query) {
SearchRequest.Builder builder = new SearchRequest.Builder()
.index(elasticProperties.getIndices())
.size(20)
.minScore(elasticProperties.getMinScore())
.knn(knn -> knn
.field("embeddings")
.k(20)
.numCandidates(100)
.queryVectorBuilder(qvb -> qvb
.textEmbedding(te -> te
.modelId(elasticProperties.getModelId())
.modelText(query)
)
)
.filter(f -> f
.bool(b -> b
.must(fullTextQuery(query))
.must(fuzzyTextQuery(query))
.minimumShouldMatch(
elasticProperties.getSimilarityThreshold())
)
)
);
SearchRequest searchRequest = builder.build();
SearchResponse<SearchResult> response =
elasticsearchClient.search(searchRequest, SearchResult.class);
...
}
How This Works
- kNN performs semantic similarity ranking
- Elasticsearch generates embedding using
modelId - Results are ranked by vector similarity
- Full-text + fuzzy act as filters (must match)
minScoreremoves weak semantic matches
Why This Is Better Than Hybrid Scoring?
- Cleaner ranking logic
- No unpredictable score blending
- Semantic intent drives results
- Lexical filters ensure relevance
Full Text vs Fuzzy vs Semantic
| Search Type | Ranking Based On | Best For |
|---|---|---|
| Full Text | BM25 | Keyword match |
| Fuzzy | Edit distance | Typo handling |
| kNN Semantic | Vector similarity | Intent understanding |
Conclusion
We implemented a production-ready Spring Boot Elasticsearch vector search using:
- Official Java Client 9.2.5
- Semantic kNN search
- BM25 filtering
- Fuzzy filtering
This architecture provides:
- High relevance
- Better semantic understanding
- Strong control over search quality
- Production scalability