← Kursa Dön
📄 Text · 35 min

Spring Data Elasticsearch — Repository Pattern

Giriş — ORM'in Arama Motoru Versiyonu

Spring Data JPA'yı biliyorsanız, Hibernate'in veritabanı ile aranızdaki tercüman olduğunu da bilirsiniz. SQL yazmadan findByEmail(String email) diye method tanımlarsınız, Hibernate gerisini halleder. Şimdi aynı konforu Elasticsearch için düşünün — CRUD, arama, pagination, aggregation... hepsini Spring'in repository pattern'ı ile yapabilirsiniz.

Spring Data Elasticsearch, Elasticsearch Java Client üzerine oturan bir soyutlama katmanıdır. Low-level client'ın gücünü kaybetmeden Spring ekosisteminin kolaylığını kazanırsınız. Bu derste sıfırdan bir Spring Boot projesi kuracak, entity tanımlayacak, repository oluşturacak ve hem derived query hem native query ile arama yapacağız.


1. Proje Kurulumu

1.1 Maven Dependencies

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.2</version>
</parent>

<properties>
    <java.version>17</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

💡 spring-boot-starter-data-elasticsearch otomatik olarak Elasticsearch Java Client, Jackson ve gerekli tüm bağımlılıkları çeker.

1.2 application.yml

spring:
  elasticsearch:
    uris: http://localhost:9200
    # Birden fazla node:
    # uris: http://node1:9200,http://node2:9200
    username: elastic         # Opsiyonel
    password: changeme        # Opsiyonel
    connection-timeout: 5s
    socket-timeout: 30s

logging:
  level:
    org.springframework.data.elasticsearch: DEBUG
    tracer: TRACE  # HTTP isteklerini logla (dev ortamı için)

1.3 Proje Yapısı

src/main/java/com/example/esdemo/
├── EsDemoApplication.java
├── document/
│   └── Product.java                # @Document entity
├── repository/
│   ├── ProductRepository.java      # ElasticsearchRepository
│   └── ProductCustomRepository.java
├── service/
│   └── ProductService.java
└── controller/
    └── ProductController.java

2. @Document Entity Tanımı

package com.example.esdemo.document;

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.time.LocalDateTime;
import java.util.List;

@Document(indexName = "products")
@Setting(shards = 3, replicas = 1)
public class Product {

    @Id
    private String id;

    @Field(type = FieldType.Text, analyzer = "standard")
    private String name;

    @Field(type = FieldType.Text, analyzer = "standard")
    private String description;

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

    @Field(type = FieldType.Double)
    private double price;

    @Field(type = FieldType.Integer)
    private int stock;

    @Field(type = FieldType.Keyword)
    private List<String> tags;

    @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
    private LocalDateTime createdAt;

    @Field(type = FieldType.Boolean)
    private boolean active;

    public Product() {}

    public Product(String name, String description, String category,
                   double price, int stock, List<String> tags) {
        this.name = name;
        this.description = description;
        this.category = category;
        this.price = price;
        this.stock = stock;
        this.tags = tags;
        this.createdAt = LocalDateTime.now();
        this.active = true;
    }

    // Getter & Setter (kısaltıldı — tüm alanlar için standart getter/setter)
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
    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 int getStock() { return stock; }
    public void setStock(int stock) { this.stock = stock; }
    public String getCategory() { return category; }
    public List<String> getTags() { return tags; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public String getDescription() { return description; }
    public boolean isActive() { return active; }
}

Annotation Rehberi

AnnotationAçıklama
@Document(indexName)Elasticsearch index adı
@IdDocument ID — String veya Long
@Field(type, analyzer)Alan tipi ve analiz ayarı
@Setting(shards, replicas)Index ayarları
@MultiField + @InnerFieldAynı alan için birden fazla mapping (text + keyword)

Multi-Field Mapping

@MultiField(
    mainField = @Field(type = FieldType.Text, analyzer = "standard"),
    otherFields = {
        @InnerField(suffix = "keyword", type = FieldType.Keyword)
    }
)
private String name;
// → name (text) + name.keyword (keyword) — hem arama hem exact match

3. ElasticsearchRepository — CRUD

3.1 Repository Interface

package com.example.esdemo.repository;

import com.example.esdemo.document.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    // Hazır metodlar: save, saveAll, findById, findAll,
    // existsById, count, deleteById, deleteAll
}

3.2 Service ile CRUD Kullanımı

package com.example.esdemo.service;

import com.example.esdemo.document.Product;
import com.example.esdemo.repository.ProductRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class ProductService {

    private final ProductRepository repository;

    public ProductService(ProductRepository repository) {
        this.repository = repository;
    }

    public Product save(Product product) {
        return repository.save(product); // ID yoksa otomatik üretilir
    }

    public Iterable<Product> saveAll(List<Product> products) {
        return repository.saveAll(products); // Bulk insert
    }

    public Optional<Product> findById(String id) {
        return repository.findById(id);
    }

    public Product update(String id, Product updated) {
        return repository.findById(id).map(existing -> {
            existing.setName(updated.getName());
            existing.setPrice(updated.getPrice());
            existing.setStock(updated.getStock());
            return repository.save(existing); // Aynı ID ile save = update
        }).orElseThrow(() -> new RuntimeException("Not found: " + id));
    }

    public void deleteById(String id) {
        repository.deleteById(id);
    }
}

⚠️ Spring Data Elasticsearch'te partial update yoksave() tüm document'ı yeniden yazar. Partial update için ElasticsearchOperations kullanın (bölüm 6).


4. Derived Query Methods

Spring Data'nın en büyük sihri: method adından otomatik sorgu üretimi.

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {

    // Kategoriye göre bul
    List<Product> findByCategory(String category);

    // Fiyat aralığı
    List<Product> findByPriceBetween(double min, double max);

    // Stoku 0'dan büyük
    List<Product> findByStockGreaterThan(int stock);

    // İsimde kelime arama (match query)
    List<Product> findByNameContaining(String keyword);

    // Aktif, fiyata göre sıralı
    List<Product> findByActiveTrueOrderByPriceAsc();

    // Kategori + fiyat filtresi
    List<Product> findByCategoryAndPriceLessThan(String category, double maxPrice);

    // Tag arama
    List<Product> findByTagsContaining(String tag);

    // Pagination destekli
    Page<Product> findByCategory(String category, Pageable pageable);

    // İsim VEYA açıklamada arama
    List<Product> findByNameContainingOrDescriptionContaining(
        String nameKey, String descKey);

    // Sayma ve varlık
    long countByCategory(String category);
    boolean existsByNameAndCategory(String name, String category);
    void deleteByCategory(String category);
}

Keyword Tablosu

KeywordES QueryÖrnek
BetweenrangefindByPriceBetween(min, max)
LessThan/GreaterThanrangefindByPriceLessThan(max)
Containingmatch/wildcardfindByNameContaining(kw)
And / Orbool must/shouldfindByNameAndCategory(...)
Notmust_notfindByCategoryNot(cat)
OrderBy...Asc/DescsortfindByXOrderByPriceDesc()
True/Falseterm (bool)findByActiveTrue()
IntermsfindByCategoryIn(List)

5. @Query Annotation — Native Query

Derived query yetmediğinde, doğrudan Elasticsearch JSON sorgusu:

import org.springframework.data.elasticsearch.annotations.Query;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {

    // Native query — ?0, ?1, ?2 = parametreler
    @Query("""
        {
            "bool": {
                "must": [{ "match": { "name": "?0" } }],
                "filter": [
                    { "term": { "category": "?1" } },
                    { "range": { "price": { "lte": ?2 } } }
                ]
            }
        }
        """)
    Page<Product> searchProducts(String keyword, String category,
                                  double maxPrice, Pageable pageable);

    // Fuzzy search
    @Query("""
        {
            "multi_match": {
                "query": "?0",
                "fields": ["name^3", "description", "tags^2"],
                "fuzziness": "AUTO"
            }
        }
        """)
    List<Product> fuzzySearch(String keyword);

    // Stokta olanlar
    @Query("""
        {
            "bool": {
                "must": { "match_all": {} },
                "filter": { "range": { "stock": { "gt": 0 } } }
            }
        }
        """)
    Page<Product> findInStock(Pageable pageable);
}

6. ElasticsearchOperations — Gelişmiş Sorgular

Repository yetmediğinde (aggregation, highlight, custom scoring) ElasticsearchOperations devreye girer.

6.1 NativeQuery ile Arama

package com.example.esdemo.service;

import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.json.JsonData;
import com.example.esdemo.document.Product;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.stereotype.Service;

@Service
public class AdvancedSearchService {

    private final ElasticsearchOperations operations;

    public AdvancedSearchService(ElasticsearchOperations operations) {
        this.operations = operations;
    }

    public SearchHits<Product> search(String keyword, String category,
                                       Double minPrice, Double maxPrice,
                                       int page, int size) {

        NativeQuery query = NativeQuery.builder()
            .withQuery(q -> q.bool(b -> {
                if (keyword != null && !keyword.isBlank()) {
                    b.must(m -> m.multiMatch(mm -> mm
                        .query(keyword)
                        .fields("name^3", "description", "tags^2")
                        .fuzziness("AUTO")));
                }
                if (category != null) {
                    b.filter(f -> f.term(t -> t
                        .field("category").value(category)));
                }
                if (minPrice != null || maxPrice != null) {
                    b.filter(f -> f.range(r -> {
                        r.field("price");
                        if (minPrice != null) r.gte(JsonData.of(minPrice));
                        if (maxPrice != null) r.lte(JsonData.of(maxPrice));
                        return r;
                    }));
                }
                b.filter(f -> f.range(r -> r.field("stock").gt(JsonData.of(0))));
                return b;
            }))
            .withSort(s -> s.field(f -> f.field("price").order(SortOrder.Asc)))
            .withPageable(PageRequest.of(page, size))
            .build();

        return operations.search(query, Product.class);
    }
}

6.2 SearchHits Kullanımı

SearchHits<Product> searchHits = service.search(
    "laptop", "electronics", null, 80000.0, 0, 10);

System.out.println("Toplam: " + searchHits.getTotalHits());
System.out.println("Max score: " + searchHits.getMaxScore());

for (SearchHit<Product> hit : searchHits.getSearchHits()) {
    Product p = hit.getContent();
    System.out.printf("[%.2f] %s — %.2f TL%n",
        hit.getScore(), p.getName(), p.getPrice());

    // Highlight
    hit.getHighlightField("name")
        .forEach(h -> System.out.println("  → " + h));
}

6.3 Highlight ile Arama

import org.springframework.data.elasticsearch.core.query.highlight.*;

NativeQuery query = NativeQuery.builder()
    .withQuery(q -> q.match(m -> m
        .field("description").query("profesyonel bilgisayar")))
    .withHighlightQuery(new HighlightQuery(
        new Highlight(List.of(
            new HighlightField("description",
                HighlightFieldParameters.builder()
                    .withPreTags("<em>").withPostTags("</em>")
                    .withFragmentSize(200)
                    .withNumberOfFragments(3)
                    .build()),
            new HighlightField("name")
        )), Product.class))
    .build();

SearchHits<Product> hits = operations.search(query, Product.class);
for (SearchHit<Product> hit : hits) {
    hit.getHighlightFields().forEach((field, fragments) ->
        System.out.println(field + ": " + String.join(" ... ", fragments)));
}

6.4 Aggregation

import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregations;

NativeQuery query = NativeQuery.builder()
    .withQuery(q -> q.matchAll(m -> m))
    .withMaxResults(0)  // Sadece aggregation, document çekme
    .withAggregation("categories",
        a -> a.terms(t -> t.field("category").size(20)))
    .withAggregation("avg_price",
        a -> a.avg(av -> av.field("price")))
    .build();

SearchHits<Product> result = operations.search(query, Product.class);
ElasticsearchAggregations aggs = (ElasticsearchAggregations)
    result.getAggregations();

// Terms aggregation parse
aggs.get("categories").aggregation().getAggregate().sterms()
    .buckets().array().forEach(bucket ->
        System.out.printf("  %s: %d%n",
            bucket.key().stringValue(), bucket.docCount()));

// Avg
double avg = aggs.get("avg_price").aggregation()
    .getAggregate().avg().value();
System.out.printf("Ortalama fiyat: %.2f%n", avg);

7. Pagination

7.1 Repository ile Pagination

import org.springframework.data.domain.*;

@Service
public class PaginatedProductService {

    private final ProductRepository repository;

    public PaginatedProductService(ProductRepository repository) {
        this.repository = repository;
    }

    public Page<Product> getProducts(int page, int size, String sortField) {
        Pageable pageable = PageRequest.of(page, size,
            Sort.by(Sort.Direction.ASC, sortField));

        Page<Product> result = repository.findByCategory("electronics", pageable);

        System.out.println("Toplam: " + result.getTotalElements());
        System.out.println("Sayfa: " + result.getNumber() + "/" + result.getTotalPages());
        System.out.println("İlk: " + result.isFirst() + ", Son: " + result.isLast());

        return result;
    }
}

7.2 REST Controller

package com.example.esdemo.controller;

import com.example.esdemo.document.Product;
import com.example.esdemo.repository.ProductRepository;
import com.example.esdemo.service.ProductService;
import org.springframework.data.domain.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService service;
    private final ProductRepository repository;

    public ProductController(ProductService service, ProductRepository repository) {
        this.service = service;
        this.repository = repository;
    }

    @GetMapping
    public Page<Product> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(defaultValue = "price") String sort) {
        return repository.findAll(PageRequest.of(page, size, Sort.by(sort)));
    }

    @GetMapping("/search")
    public List<Product> search(@RequestParam String q) {
        return repository.fuzzySearch(q);
    }

    @PostMapping
    public Product create(@RequestBody Product product) {
        return service.save(product);
    }

    @GetMapping("/{id}")
    public Product get(@PathVariable String id) {
        return service.findById(id)
            .orElseThrow(() -> new RuntimeException("Not found: " + id));
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable String id) {
        service.deleteById(id);
    }
}

8. Custom Repository Implementation

Derived query ve @Query yetmediğinde, custom implementation ile tam kontrol:

// 1. Custom interface
public interface ProductCustomRepository {
    SearchHits<Product> advancedSearch(String keyword, String category,
                                       Double minPrice, Double maxPrice,
                                       int page, int size);
}
// 2. Implementation — sınıf adı "Impl" ile bitmeli!
import co.elastic.clients.json.JsonData;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.stereotype.Component;

@Component
public class ProductCustomRepositoryImpl implements ProductCustomRepository {

    private final ElasticsearchOperations operations;

    public ProductCustomRepositoryImpl(ElasticsearchOperations operations) {
        this.operations = operations;
    }

    @Override
    public SearchHits<Product> advancedSearch(String keyword, String category,
                                               Double minPrice, Double maxPrice,
                                               int page, int size) {
        NativeQuery query = NativeQuery.builder()
            .withQuery(q -> q.bool(b -> {
                if (keyword != null) {
                    b.must(m -> m.multiMatch(mm -> mm
                        .query(keyword).fields("name^3", "description")
                        .fuzziness("AUTO")));
                }
                if (category != null) {
                    b.filter(f -> f.term(t -> t.field("category").value(category)));
                }
                if (minPrice != null) {
                    b.filter(f -> f.range(r -> r.field("price").gte(JsonData.of(minPrice))));
                }
                if (maxPrice != null) {
                    b.filter(f -> f.range(r -> r.field("price").lte(JsonData.of(maxPrice))));
                }
                return b;
            }))
            .withPageable(PageRequest.of(page, size))
            .build();

        return operations.search(query, Product.class);
    }
}
// 3. Ana repository'ye ekle — derived + custom birlikte
public interface ProductRepository
        extends ElasticsearchRepository<Product, String>,
                ProductCustomRepository {
    // Tüm derived query'ler + custom advancedSearch
}

9. Bütünleşik Örnek — CommandLineRunner

package com.example.esdemo;

import com.example.esdemo.document.Product;
import com.example.esdemo.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.List;

@SpringBootApplication
public class EsDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(EsDemoApplication.class, args);
    }

    @Bean
    CommandLineRunner initData(ProductRepository repository) {
        return args -> {
            repository.deleteAll();

            repository.saveAll(List.of(
                new Product("MacBook Pro 16", "Apple M3 Max laptop",
                    "electronics", 74999.99, 50, List.of("apple", "laptop")),
                new Product("iPhone 15 Pro", "A17 Pro telefon",
                    "electronics", 64999.99, 100, List.of("apple", "phone")),
                new Product("Samsung Galaxy S24", "AI telefon",
                    "electronics", 49999.99, 80, List.of("samsung", "phone")),
                new Product("Sony WH-1000XM5", "ANC kulaklık",
                    "electronics", 12999.99, 200, List.of("sony", "headphone")),
                new Product("Clean Code", "Robert C. Martin",
                    "books", 349.99, 300, List.of("java", "best-practice"))
            ));
            System.out.println("✅ Veriler yüklendi");

            // Derived query örnekleri
            System.out.println("\n--- Electronics ---");
            repository.findByCategory("electronics")
                .forEach(p -> System.out.printf("  %s — %.2f TL%n", p.getName(), p.getPrice()));

            System.out.println("\n--- 10K-60K arası ---");
            repository.findByPriceBetween(10000, 60000)
                .forEach(p -> System.out.printf("  %s — %.2f TL%n", p.getName(), p.getPrice()));

            System.out.println("\n--- 'apple' tag ---");
            repository.findByTagsContaining("apple")
                .forEach(p -> System.out.printf("  %s%n", p.getName()));

            System.out.println("\nToplam: " + repository.count());
            System.out.println("Elektronik: " + repository.countByCategory("electronics"));
        };
    }
}

10. Integration Test

package com.example.esdemo;

import com.example.esdemo.document.Product;
import com.example.esdemo.repository.ProductRepository;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ProductRepositoryTest {

    @Autowired
    private ProductRepository repository;

    @BeforeEach
    void setup() {
        repository.deleteAll();
        repository.saveAll(List.of(
            new Product("MacBook Pro", "Apple laptop", "electronics",
                74999.99, 50, List.of("apple", "laptop")),
            new Product("iPhone 15", "Apple phone", "electronics",
                64999.99, 100, List.of("apple", "phone")),
            new Product("Clean Code", "Programming book", "books",
                349.99, 300, List.of("java"))
        ));
    }

    @Test
    void testSaveAndFind() {
        Product saved = repository.save(new Product("Test", "Desc",
            "test", 99.99, 10, List.of("test")));
        assertNotNull(saved.getId());
        assertTrue(repository.findById(saved.getId()).isPresent());
    }

    @Test
    void testFindByCategory() {
        assertEquals(2, repository.findByCategory("electronics").size());
    }

    @Test
    void testPriceBetween() {
        List<Product> result = repository.findByPriceBetween(50000, 70000);
        assertEquals(1, result.size());
        assertEquals("iPhone 15", result.get(0).getName());
    }

    @Test
    void testPagination() {
        Page<Product> page = repository.findByCategory("electronics",
            PageRequest.of(0, 1));
        assertEquals(2, page.getTotalElements());
        assertEquals(2, page.getTotalPages());
        assertEquals(1, page.getContent().size());
    }

    @Test
    void testCount() {
        assertEquals(3, repository.count());
        assertEquals(2, repository.countByCategory("electronics"));
    }
}

11. Best Practices

✅ Yapın

UygulamaNeden
Derived query ile başlayınBasit sorgular için en temiz yol
Karmaşık sorgularda NativeQueryElasticsearch'ün tam gücü
Aggregation → ElasticsearchOperationsRepository aggregation desteklemez
@Field annotation'larını açık yazınOtomatik mapping sürpriz yapabilir
Integration test yazınMapping + sorgu hatalarını erkenden yakalayın

❌ Yapmayın

UygulamaNeden
Partial update için save() kullanmayınTüm document'ı yeniden yazar
Production'da createIndex = true bırakmayınIndex yönetimi CI/CD ile olmalı
findAll() ile tüm veriyi çekmeyinMilyon document = OutOfMemory
@Query'de kullanıcı girdisini direkt koymayınInjection riski

12. Yaygın Hatalar

Hata 1: Field Type Uyumsuzluğu

// ❌ Java int ↔ Elasticsearch text → mapping hatası
@Field(type = FieldType.Text)
private int stock;

// ✅ Doğru tip eşlemesi
@Field(type = FieldType.Integer)
private int stock;
// ❌ Keyword alanda "Containing" beklendiği gibi çalışmaz
@Field(type = FieldType.Keyword)
private String name;
// findByNameContaining("Mac") → bulamaz

// ✅ Text alan kullanın
@Field(type = FieldType.Text)
private String name;

Hata 3: Lazy Refresh

// ❌ save() sonrası hemen arama
repository.save(product);
repository.findByName("test"); // Boş dönebilir!

// ✅ Refresh bekleyin
repository.save(product);
operations.indexOps(Product.class).refresh();
repository.findByName("test"); // Bulur

Hata 4: Custom Repository Adı

// ❌ Impl suffix yok → Spring tanımaz
public class ProductCustomSearch implements ProductCustomRepository { }

// ✅ "Impl" ile bitmeli
public class ProductCustomRepositoryImpl implements ProductCustomRepository { }

Özet

  • Spring Data Elasticsearch, ES Java Client üzerine kurulu soyutlama — ElasticsearchRepository ile JPA benzeri CRUD

  • @Document ile entity, @Field ile mapping — FieldType.Text, Keyword, Date, Integer gibi tipler

  • Derived query ile method adından otomatik sorgu: findByCategory, findByPriceBetween, findByNameContaining

  • @Query annotation ile native ES JSON sorgusu — derived query'nin yetemediği yerde

  • NativeQuery builder ile programatik karmaşık sorgular — bool, aggregation, highlight, sort

  • Pagination: Pageable + Page<T>PageRequest.of(page, size, sort) ile

  • Custom Repository ile ElasticsearchOperations kullanarak tam kontrol — aggregation, highlight desteği

  • save() = full replace — partial update için ElasticsearchOperations kullanın