Merhabalar. Bu yazıda Spring Data Elasticsearch ile Spring Boot ve Elasticsearch’ün birlikte kullanımını anlatacağım. Bu yazıdaki kodları içeren örnek projeye https://github.com/ilkgunel/SpringDataElasticsearchDemo adresinden erişebilirsiniz. Şimdi başlayalım.

Elasticsearch Nedir?

Kaba taslak Elasticsearch’den bahsedecek olursak MongoDB gibi, Cassandra gibi Elasticsearch de bir açık kaynak kodlu NoSQL veritabanıdır. Elastichsearch’ü diğerlerinden ayıran nokta altyapısında arama motorlarının da alt yapısını barındıran Apache Lucene adındaki bir projenin olmasıdır. Bu proje sayesinde Elastichsearch indexleme ve arama işlemlerinde çok daha hızlı ve efektif iş yapmayı sağlamaktadır.

Elasticsearch’e her bir kayıt girişinde bu kayıt içerisinde yer alan alanlar Elastichsearch tarafından indexlenir. Bu sayede elasticsearch aranmak üzere gönderilen text’i veri kümeleri içinde aramak yerine index içinde arar, geçtiği yerleri hızlıca bulur ve döner.

Elasticsearch içerisindeki index kavramını klasik RDMS’teki veritabanına karşılık geliyor şeklinde düşünebiliriz.

Bu index kavramının altında type isminde bir kavram vardır, bu RDMS’teki tabloya karşılık geliyor diye düşünebiliriz.

NOT: Type özelliği 7.x.x versiyonu ile deprecated hale gelmiştir ve Elastichsearch'den kaldırılmak üzeredir. Elasticsearch bu konuda ya her doküman tipi için index kullanımını ya da kendi custom type'larınızı oluşturmanızı tavsiye etmektedir.

type dediğimiz kavramın altında document adında bir kavram vardır. Bu kavram da RDMS’teki satırlara yani kayıtlara karşılık gelmektedir. Elasticsearch içerisindeki her bir doküman JSON formantında saklanmaktadır. Her bir document içerisinde bir ya da birden fazla document’e sahip olabilir.

Document kavramı da içerisinde field adındaki kavrama sahiptir. Bu kavram da RDMS’teki kolonlara karşılık geliyor diye düşünebiliriz.

Elasticsearch’de dynamic adında bir özellik de bulunmaktadır. Klasik veritabanlarında bildiğimiz gibi tablo içerisinde tanımlı olmayan bir alan için veri gelirse ya hata verilir ya da gelen bilgi yok sayılır. Elastichsearch’de ise bu durumda nasıl hareket edilmesi gerektiği kararı bize bırakılmıştır. Eğer bu dynamic özelliği strict olursa Elastichsearch önceden tanımlı olmadığı halde gelen alan bilgilerini içeren kaydı işlemez ve hata fırlatır. Eğer bu dynamic özelliği false olursa önceden tanımlı olmadığı halde gelen alan bilgilerini içeren kaydın sadece tanımlı olan kısmı işlenir, tanımlı olmayan alanlar yok sayılır. Eğer bu dynamic özelliği true olursa gelen bilgi içerisindeki tüm alanlar işlenir, önceden tanımlı olmayanlar da yeni birer alan olarak sisteme eklenir.

Demo Uygulama

Şimdi Java dilinde Spring Boot tabanlı olacak şekilde bir demo uygulama yapalım.

Benim bilgisayarımda kurulu Elasticsearch içerisinde bir adet f1index aında index var. f1index altında da şu şekilde 1 tane kayıt tutuluyor.

Bizim örneğimiz de bu f1index’teki kayıtları okuma ve f1index üzerinde yazma, silme ve güncelleme işlemi yapacak.

Şimdi kaynak kodları inceleyelim.

Driver.java

package com.ilkaygunel.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;

@Document(indexName = "f1index")
public class Driver {

	@Id
	private String id;

	private String name;

	private String surname;

	private String team;

	public String get_id() {
		return id;
	}

	public void set_id(String id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getSurname() {
		return surname;
	}

	public void setSurname(String surname) {
		this.surname = surname;
	}

	public String getTeam() {
		return team;
	}

	public void setTeam(String team) {
		this.team = team;
	}

}

Az önce Elasticsearch’deki her bir kaydın birer Document olduğundan bahsetmiştik. Driver sınıfımız da her bir Document için temsil edici sınıfımız olacaktır.

  • Sınıfımızı @Document notasyonu ile işaretliyoruz ve notasyona parametre olarak kullanacağınız indexi indexName parametresi ile veriyoruz.
  • Elastichsearch her bir kayıt için bir id üretir. Biz de üretilen id’yi kendi tarafımıza alabilmek için id isimli alanımızı @Id notasyonu ile işaretliyoruz.
  • name, surname ve team alanlarını temsil eden değişkenlerimizi ve onlara ait get-set metotlarını da tanımlıyoruz.

DriverRepository.java

package com.ilkaygunel.repository;

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import com.ilkaygunel.model.Driver;

@Repository
public interface DriverRepository extends ElasticsearchRepository<Driver, String> {

}

DriverRepository interface’imiz bizim için veri tabanı operasyonlarını gerçeklştirecek olan yapıdır.

  • @Repository sınıfı ile interface’i işaretleyip Spring’e kaydını yaptırıyoruz.
  • interface’i ElasticsearchRepository interface’inden kalıtıyoruz. ElasticsearchRepository interface’ine parametre olarak Document’leri temsil eden sınıfı ve bu sınıftaki id’nin tipini geçiriyoruz.

DriverService.java

package com.ilkaygunel.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.ilkaygunel.model.Driver;
import com.ilkaygunel.repository.DriverRepository;

@Service
public class DriverService {

	@Autowired
	private DriverRepository driverRepository;

	public List<Driver> allDrivers() {
		List<Driver> allDrivers = new ArrayList<Driver>();

		driverRepository.findAll().forEach(driver -> {
			allDrivers.add(driver);
		});

		return allDrivers;
	}

	public void saveDriver(Driver driver) {
		driverRepository.save(driver);
	}

	public void updateDriver(String key, Driver driverForUpdate) {
		driverRepository.findById(key).ifPresentOrElse(driver -> {
			driver.setName(driverForUpdate.getName());
			driver.setSurname(driverForUpdate.getSurname());
			driver.setTeam(driverForUpdate.getTeam());
			driverRepository.save(driver);
		}, () -> {
			throw new RuntimeException("No Record With This Id!");
		});
	}

	public void deleteDriver(String key) {
		driverRepository.findById(key).ifPresentOrElse(driver -> {
			driverRepository.delete(driver);
		}, () -> {
			throw new RuntimeException("No Record With This Id!");
		});
	}

}

DriverService sınıfı bizim servis işlerimizi yapacak sınıftır.

  • Sınıfı @Service notasyonu ile işaretleyip kaydını yaptırıyoruz.
  • @Autowired notasyonu DriverRepository’i inject ediyoruz.
  • allDrivers metodu bize tüm sürücüleri dönecek.
  • saveDriver metodu yeni sürücü kaydı yapacak.
  • updateDriver metodu sürücü güncelleme işlemi yapacak.
  • deleteDriver metodu sürücü silme işlemi yapacak.

RestEndPoints.java

package com.ilkaygunel.rest;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.ilkaygunel.model.Driver;
import com.ilkaygunel.service.DriverService;

@RestController
@RequestMapping(value = "/api")
public class RestEndPoints {

	@Autowired
	private DriverService driverService;

	@RequestMapping(method = RequestMethod.GET, value = "/drivers")
	public ResponseEntity<List<Driver>> allDrivers() {
		List<Driver> allDrivers = driverService.allDrivers();
		return new ResponseEntity<List<Driver>>(allDrivers, HttpStatus.OK);
	}

	@RequestMapping(method = RequestMethod.POST, value = "/drivers")
	public ResponseEntity<String> postDriver(@RequestBody Driver driver) {
		driverService.saveDriver(driver);
		return new ResponseEntity<String>("Kayıt İşlemi Başarılı", HttpStatus.OK);
	}

	@RequestMapping(method = RequestMethod.PUT, value = "/drivers/{id}")
	public ResponseEntity<String> putDriver(@PathVariable("id") String id, @RequestBody Driver driverForUpdate) {
		driverService.updateDriver(id, driverForUpdate);
		return new ResponseEntity<String>("Güncelleme İşlemi Başarılı", HttpStatus.OK);
	}

	@RequestMapping(method = RequestMethod.DELETE, value = "/drivers/{id}")
	public ResponseEntity<String> deleteDriver(@PathVariable("id") String id) {
		driverService.deleteDriver(id);
		return new ResponseEntity<String>("Sime İşlemi Başarılı", HttpStatus.OK);
	}
}

RestEndPoints sınıfımız Rest API çağırımları için kullanılacak endpointleri barındırıyor. GET, POST, PUT ve DELETE HTTP metotları ile gelen istekleri servis kısmına iletecek endpointlerimiz yer alıyor. Bu kısımları daha önce başka yazılarda anlattığım için geçiyorum fakat sorularınız olursa yorum kısmına yazınız lütfen.

RestClientConfig.java

package com.ilkaygunel.elastichsearchconfiguration;

import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.RestClients;
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;

@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {

	@Override
	@Bean
	public RestHighLevelClient elasticsearchClient() {
		final ClientConfiguration clientConfiguration = ClientConfiguration.builder().connectedTo("localhost:9200")
				.build();

		return RestClients.create(clientConfiguration).rest();
	}

}

RestClientConfig sınıfımız Elastichsearch ile iletişime geçecek client’in kayda geçirildiği sınıftır.

  • Sınıfımızı @Configuration notasyonu ile işaretleyip Spring’e tanıtıyoruz.
  • Sınıfımızı AbstractElasticsearchConfiguration sınıfından kalıtıyoruz.
  • AbstractElasticsearchConfiguration sınıfından gelen elasticsearchClient metodunu Override ediyoruz ve aynı zamanda RestHighLevelClient tipindeki bir objenin Spring tarafından kullanılabilmesi için @Bean notasyonu ile işaretliyoruz.
  • elasticsearchClient metodu içerisinde Elasticsearch’ün çalıştığı host ve port bilgisini connectedTo metoduna vererek ClientConfiguration tipinde bir obje elde ediyoruz.
  • Son olarak da RestClients sınıfındaki create metodunu ClientConfiguration tipindeki objeyi parametre vererek ve rest() metodunu çağırarak client objemizi elde etmiş oluyoruz.

Application.java

package com.ilkaygunel.application;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;

@SpringBootApplication
@ComponentScan("com.ilkaygunel.*")
@EnableElasticsearchRepositories(basePackages = "com.ilkaygunel.repository")
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

Application sınıfımız uygulamamızı ayağa kaldıracağımız sınıfımızdır.

  • Sınıfımızı @SpringBootApplication notasyonu ile işaretliyoruz.
  • @ComponentScan(“com.ilkaygunel.*”) notasyonu ile com.ilkaygunel ve alt paketlerinin Spring tarafından taranmasını söylüyoruz.
  • @EnableElasticsearchRepositories notasyonu ile de Elasticsearch için yazılmış repository’nin yer aldığı paketi kayda geçiyoruz. Bu paket altındaki repository’ler Spring tarafından içi doldurularak kullanıma hazır hale getirilecek.

DEMO

Şimdi ilk olarak http://localhost:8080/api/drivers/ adresine GET isteği gönderiyorum ve aşağıdaki gibi bir sonuç alıyorum. f1index’teki kayıtlı document’i bize dönüyor.

Şimdi de http://localhost:8080/api/drivers/ adresine ekran görüntüsündeki body ile birlikte POST isteği gönderiyorum. Sonuç olarak bana kayıt işleminin başarılı olduğuna dair mesaj dönüyor. http://localhost:8080/api/drivers/ adresine tekrar GET isteği gönderdiğimde az önce eklediğim kayıt da dönen liste içerisinde yer alıyor.

Şimdi de üye güncelleme işlemine bakalım.

http://localhost:8080/api/drivers/ adresine bir id bilgisi ekleyerek HTTP PUT isteğinde bulunacağım. Burada Charles Leclerc kullanıcısının id bilgisi olan _-KjOXMBsY0W27BzERPb bilgisini kullanarak PUT isteğinde bulunacağım. Bu durumda http://localhost:8080/api/drivers/_-KjOXMBsY0W27BzERPb adresine bir PUT isteği göndereceğim. İsteği aşağıdaki gibi gönderiyorum ve bana üye güncelleme işleminin başarılı olduğuna dair bir mesaj dönüyor. http://localhost:8080/api/drivers/ adresine tekrar bir GET isteği gönderiyorum. Charles Leclerc’in takım bilgisinin güncellendiğini görüyorum.

Şimdi son olarak da silme işlemine bakalım.

Yine http://localhost:8080/api/drivers/ adresine bir id bilgisi ekleyeceğim ve elde ettiğim URL ile HTTP DELETE methodu çağırımı yapacağım. Güncelleme işleminden farklı olarak bir body göndermeyeceğim, sistem id bilgisi ile eşleşen kaydı bulup silecek. Alex Albon kullanıcısının id bilgisi olan BOLMOXMBsY0W27Bz7xSa bilgisini kullanarak http://localhost:8080/api/drivers/BOLMOXMBsY0W27Bz7xSa adresini elde ediyorum ve aşağıdaki şekilde istekte bulunuyorum. Bana Silme İşlemi Başarılı şeklinde mesaj dönüyor. Yeniden http://localhost:8080/api/drivers/ adresine GET isteğinde bulunduğumda kaydın silinmiş olduğunu görüyorum.

Bu yazıda anlatacaklarım da bu kadar arkadaşlar. Başka bir yazıda görüşmek üzere hoşçakalın.