TL;DR
  • Vector Search: Semantic-based search that finds similar content even with different keywords
  • dense_vector: Field type for storing vectors, specify similarity calculation method
  • kNN Query: Search method to find k nearest neighbor documents
  • Hybrid Search: Combine kNN + keyword search for best results
  • Embedding Model: Converts text/images to vectors (multilingual models recommended for Korean)

Target Audience: Developers implementing semantic search or recommendation systems Prerequisites: Core Components, Query DSL, Basic ML concepts

Learn how to implement semantic search and similar image search using Elasticsearch’s vector search (kNN).

Version Requirements
  • Elasticsearch 8.0+ required (native kNN support)
  • Versions before 8.x require script_score or plugins

Traditional search is keyword matching. Searching “puppy” only finds documents containing “puppy”.

Vector Search is semantic-based search:

  • Search “puppy” → Also finds “dog”, “pet”, “canine”
  • Image search → Find similar images
  • Recommendation systems → Recommend similar products

How It Works#

flowchart LR
    A["Text/Image"] --> B["Embedding Model"]
    B --> C["Vector Conversion"]
    C --> D["Store in Elasticsearch"]

    E["Search Query"] --> F["Embedding Model"]
    F --> G["Query Vector"]
    G --> H["kNN Search"]
    D --> H
    H --> I["Return Similar Documents"]

Diagram: Text/images are converted to vectors through an embedding model and stored in Elasticsearch. Search queries are vectorized the same way, and kNN finds similar documents.

  1. Embedding: Convert text/image to high-dimensional vector
  2. Storage: Store vector in Elasticsearch dense_vector field
  3. Search: Find closest documents to query vector using kNN algorithm
Key Points
  • Vector Search finds documents by “meaning” not keywords, so it also finds synonyms and similar expressions
  • The embedding model converts text to vectors; model selection is crucial for search quality
  • Query must be vectorized using the same model during search

Index Configuration#

dense_vector Field Definition#

PUT /products-vector
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "description": {
        "type": "text"
      },
      "description_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}

Key Settings#

OptionDescriptionRecommended
dimsVector dimensionsDepends on model (384, 768, 1536, etc.)
indexCreate kNN indextrue (for search)
similaritySimilarity calculation methodcosine (normalized vectors), dot_product, l2_norm

similarity Options#

MethodDescriptionWhen to Use
cosineCosine similarityMost text embeddings (default)
dot_productInner productAlready normalized vectors (faster)
l2_normEuclidean distanceDistance-based similarity
Key Points
  • dims must match the embedding model’s dimension count
  • Set index: true to enable kNN search
  • Use similarity: cosine for most text embeddings

Document Indexing#

Embedding Generation (Python Example)#

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

text = "MacBook Pro 14-inch with Apple M3 Pro chip"
vector = model.encode(text).tolist()  # 384-dimension vector

Store in Elasticsearch#

PUT /products-vector/_doc/1
{
  "name": "MacBook Pro 14-inch",
  "description": "Premium laptop with Apple M3 Pro chip",
  "description_vector": [0.12, -0.34, 0.56, ...]  // 384 floats
}

Bulk Indexing#

POST /_bulk
{"index": {"_index": "products-vector", "_id": "1"}}
{"name": "MacBook Pro", "description_vector": [0.12, -0.34, ...]}
{"index": {"_index": "products-vector", "_id": "2"}}
{"name": "Galaxy Book", "description_vector": [0.08, -0.21, ...]}
Key Points
  • Embeddings must be generated by external service or library before indexing
  • Vector field requires exactly dims number of float values
  • Use Bulk API for large-scale indexing

Basic kNN Query#

GET /products-vector/_search
{
  "knn": {
    "field": "description_vector",
    "query_vector": [0.15, -0.30, 0.52, ...],  // Search vector
    "k": 10,
    "num_candidates": 100
  }
}
ParameterDescription
kNumber of nearest neighbors to return
num_candidatesNumber of candidate documents per shard (accuracy↑ = performance↓)

kNN + Filter Combination#

GET /products-vector/_search
{
  "knn": {
    "field": "description_vector",
    "query_vector": [0.15, -0.30, ...],
    "k": 10,
    "num_candidates": 100,
    "filter": {
      "bool": {
        "must": [
          { "term": { "category": "laptop" } },
          { "range": { "price": { "lte": 2000000 } } }
        ]
      }
    }
  }
}
GET /products-vector/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "name": {
              "query": "MacBook",
              "boost": 0.3
            }
          }
        }
      ]
    }
  },
  "knn": {
    "field": "description_vector",
    "query_vector": [0.15, -0.30, ...],
    "k": 10,
    "num_candidates": 100,
    "boost": 0.7
  }
}

Hybrid Search: Combines keyword matching (precision) with semantic search (relevance) for best results

Key Points
  • k: Number of results to return, num_candidates: accuracy vs speed trade-off
  • Use filter to filter conditions before kNN search
  • When combining keyword + semantic search, adjust weights with boost

Spring Boot Implementation#

Product.java#

@Document(indexName = "products-vector")
public class Product {

    @Id
    private String id;

    @Field(type = FieldType.Text)
    private String name;

    @Field(type = FieldType.Text)
    private String description;

    @Field(type = FieldType.Dense_Vector, dims = 384)
    private float[] descriptionVector;

    @Field(type = FieldType.Keyword)
    private String category;

    @Field(type = FieldType.Integer)
    private Integer price;

    // getters, setters
}

VectorSearchService.java#

@Service
public class VectorSearchService {

    private final ElasticsearchOperations operations;
    private final EmbeddingService embeddingService;

    /**
     * Semantic search
     */
    public List<Product> semanticSearch(String query, int k) {
        // 1. Convert query to vector
        float[] queryVector = embeddingService.embed(query);

        // 2. kNN search
        NativeQuery nativeQuery = NativeQuery.builder()
            .withKnnQuery(KnnQuery.builder()
                .field("description_vector")
                .queryVector(queryVector)
                .k(k)
                .numCandidates(100)
                .build()
            )
            .build();

        return operations.search(nativeQuery, Product.class)
            .getSearchHits().stream()
            .map(SearchHit::getContent)
            .toList();
    }

    /**
     * Hybrid search (kNN + keyword)
     */
    public List<Product> hybridSearch(String query, int k) {
        float[] queryVector = embeddingService.embed(query);

        NativeQuery nativeQuery = NativeQuery.builder()
            .withQuery(Query.of(q -> q
                .bool(b -> b
                    .should(Query.of(sq -> sq
                        .match(m -> m
                            .field("name")
                            .query(query)
                            .boost(0.3f)
                        )
                    ))
                )
            ))
            .withKnnQuery(KnnQuery.builder()
                .field("description_vector")
                .queryVector(queryVector)
                .k(k)
                .numCandidates(100)
                .boost(0.7f)
                .build()
            )
            .build();

        return operations.search(nativeQuery, Product.class)
            .getSearchHits().stream()
            .map(SearchHit::getContent)
            .toList();
    }

    /**
     * Similar product recommendations
     */
    public List<Product> findSimilar(String productId, int k) {
        // Get reference product's vector
        Product product = operations.get(productId, Product.class);
        if (product == null || product.getDescriptionVector() == null) {
            return List.of();
        }

        NativeQuery nativeQuery = NativeQuery.builder()
            .withKnnQuery(KnnQuery.builder()
                .field("description_vector")
                .queryVector(product.getDescriptionVector())
                .k(k + 1)  // Exclude self
                .numCandidates(100)
                .build()
            )
            .build();

        return operations.search(nativeQuery, Product.class)
            .getSearchHits().stream()
            .map(SearchHit::getContent)
            .filter(p -> !p.getId().equals(productId))  // Exclude self
            .limit(k)
            .toList();
    }
}

EmbeddingService.java#

@Service
public class EmbeddingService {

    private final RestTemplate restTemplate;

    // Call external embedding API (e.g., OpenAI, HuggingFace)
    public float[] embed(String text) {
        EmbeddingRequest request = new EmbeddingRequest(text);
        EmbeddingResponse response = restTemplate.postForObject(
            "http://embedding-service/embed",
            request,
            EmbeddingResponse.class
        );
        return response.getVector();
    }

    // Batch embedding
    public List<float[]> embedBatch(List<String> texts) {
        // ... batch processing
    }
}
Key Points
  • An EmbeddingService is needed to convert queries to vectors during search
  • Similar product recommendations use existing product’s vector as query vector
  • Map with Spring Data Elasticsearch’s @Field(type = FieldType.Dense_Vector)

Embedding Model Selection#

ModelDimensionsCharacteristicsUse Case
all-MiniLM-L6-v2384Fast, lightweightGeneral text
all-mpnet-base-v2768High qualityPrecision search
text-embedding-ada-002 (OpenAI)1536Highest qualityProduction
multilingual-e5-large1024Multilingual supportKorean search

Korean Search Tip: Use multilingual models (multilingual-e5-*) or Korean-specific models

Key Points
  • Consider dimensions, quality, speed, and cost when selecting models
  • Multilingual models (multilingual-e5-*) or Korean-specific models recommended for Korean search
  • OpenAI API (text-embedding-ada-002) has high quality but incurs costs

Performance Optimization#

Indexing Performance#

PUT /products-vector
{
  "mappings": {
    "properties": {
      "description_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "dot_product",
        "index_options": {
          "type": "hnsw",
          "m": 16,
          "ef_construction": 100
        }
      }
    }
  }
}
HNSW ParameterDescriptionTrade-off
mConnections per nodeHigher = more accurate↑, memory↑
ef_constructionSearch range during index buildHigher = more accurate↑, indexing speed↓

Search Performance#

{
  "knn": {
    "field": "description_vector",
    "query_vector": [...],
    "k": 10,
    "num_candidates": 50  // Accuracy vs speed trade-off
  }
}
  • Lower num_candidates → Faster but less accurate
  • Higher num_candidates → More accurate but slower
Key Points
  • Adjust indexing accuracy and speed with HNSW parameters (m, ef_construction)
  • Adjust search accuracy vs speed trade-off with num_candidates
  • dot_product is faster than cosine for normalized vectors

Use Cases#

Search “lightweight work laptop” → Returns products related to weight, battery, performance

2. Similar Product Recommendations#

Display “Similar Products” on product detail page

Image embedding → Find similar images (fashion, interior)

4. FAQ Bot#

Question embedding → Return most similar FAQ answer

Key Points
  • Semantic search: Natural language queries for meaning-based search
  • Similar product/image recommendations: Search for similar items using existing item’s vector
  • FAQ/chatbot: Vectorize questions to return most similar answers

Next Steps#

GoalRecommended Document
Improve search qualitySearch Relevance
Basic searchQuery DSL
Performance optimizationPerformance Tuning