[Spring] Elasticsearch + Spring Boot๋ก ๊ฒ์ ๊ธฐ๋ฅ ๊ตฌํ
Elasticsearch๋?
Elasticsearch๋ ๋์ฉ๋ ๋ฐ์ดํฐ์์ ๋น ๋ฅด๊ณ ํจ์จ์ ์ธ ๊ฒ์์ ์ํํ ์ ์๋๋ก ์ค๊ณ๋ ์คํ ์์ค ๋ถ์ฐ ๊ฒ์ ์์ง์ ๋๋ค. JSON ๋ฌธ์ ๊ธฐ๋ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ ๊ฒ์ํ๋ฉฐ, ์ญ์์ธ(Inverted Index) ๋ฐฉ์์ ์ฌ์ฉํด ๋น ๋ฅธ ๊ฒ์์ ์ง์ํฉ๋๋ค.
Elasticsearch์ ์ฃผ์ ํน์ง
- ์ ๋ฌธ ๊ฒ์(Full Text Search)
- ํํ์ ๋ถ์์ ํตํด ํ ์คํธ ๊ฒ์์ ์ต์ ํํ๊ณ ๋์์ด ๋ฐ ์ ์์ด ๊ฒ์์ ์ง์ํฉ๋๋ค.
- ์ค์๊ฐ ๊ฒ์
- ๋๋์ ๋ฐ์ดํฐ๋ฅผ ๋น ๋ฅด๊ฒ ์ฒ๋ฆฌ ๋ฐ ๊ฒ์ํ ์ ์์ต๋๋ค.
- ๋ถ์ฐ ์์คํ
- ์ฌ๋ฌ ๊ฐ์ ๋ ธ๋๋ก ํด๋ฌ์คํฐ๋ฅผ ๊ตฌ์ฑํ์ฌ ํ์ฅ์ฑ์ ์ ๊ณตํฉ๋๋ค.
- RESTful API ์ง์
- GET, POST, DELETE ๋ฑ์ REST API๋ฅผ ํ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์์ฝ๊ฒ CRUD(์์ฑ, ์กฐํ, ์์ , ์ญ์ )ํ ์ ์์ต๋๋ค.
- ์ญ์์ธ ๊ตฌ์กฐ(Inverted Index)
- ์์ธ๋ ๋ฐ์ดํฐ๋ฅผ ํ์ฉํ์ฌ ํน์ ๋จ์ด์ ๊ด๋ จ๋ ๋ฌธ์๋ฅผ ๋น ๋ฅด๊ฒ ๊ฒ์ํ ์ ์์ต๋๋ค.
- ์คํค๋ง๋ฆฌ์ค(Schema-less)
- ๋ฏธ๋ฆฌ ์ ์๋ ์คํค๋ง ์์ด๋ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก ๋ถ์ํ์ฌ ํ๋๋ฅผ ์์ฑํ๊ณ ์ ์ฅํ ์ ์์ต๋๋ค.
- ๊ฐ๋ ฅํ ๋ฐ์ดํฐ ๋ถ์ ๊ธฐ๋ฅ
- ์ง๊ณ(Aggregation), ๋์์ด ์ฒ๋ฆฌ, ์๋ ์์ฑ ๋ฑ์ ๊ธฐ๋ฅ์ ์ง์ํ์ฌ ์ ๊ตํ ๋ฐ์ดํฐ ๋ถ์์ด ๊ฐ๋ฅํฉ๋๋ค.
Elasticsearch์ ํ์ฉ ์ฌ๋ก
- ๊ฒ์ ์์ง (์น์ฌ์ดํธ, ์ ์์๊ฑฐ๋, ๋ธ๋ก๊ทธ ๋ฑ)
- ๋ก๊ทธ ๋ฐ ๋ชจ๋ํฐ๋ง ์์คํ (ELK Stack: Elasticsearch + Logstash + Kibana)
- ๋ฐ์ดํฐ ๋ถ์ ๋ฐ ์๊ฐํ
- ์ถ์ฒ ์์คํ
RDBMS์์ ์ฐจ์ด์
๊ธฐ์กด RDBMS ๊ธฐ๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ํ ์คํธ ๊ฒ์์ด ์ด๋ ต์ต๋๋ค. ์ผ๋ฐ์ ์ผ๋ก LIKE ๊ฒ์์ ์์กดํด์ผ ํ์ง๋ง, LIKE ๊ฒ์์ ๋์์ด๋ ์ ์์ด๋ฅผ ์ง์ํ์ง ์์ ํ๊ณ๊ฐ ์์ต๋๋ค. ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด Elasticsearch(ES) ๋ฅผ ํ์ฉํ๋ฉด ๋ณด๋ค ๊ฐ๋ ฅํ ํ ์คํธ ๊ฒ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ ์ ์์ต๋๋ค.
Elasticsearch๋ ์ญ์์ธ(Inverted Index) ๊ตฌ์กฐ ๋ฅผ ํ์ฉํ์ฌ ๋น ๋ฅธ ๊ฒ์์ ์ง์ํ๋ฉฐ, ํํ์ ๋ถ์ ์ ํตํด ๊ฒ์ ์ฑ๋ฅ์ ๊ทน๋ํํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ณด๋ค ์ ํํ๊ณ ์ ์ฐํ ๊ฒ์ ๊ธฐ๋ฅ ์ ์ ๊ณตํฉ๋๋ค.
๋ํ, Elasticsearch๋ ๋จ์ํ ํ ์คํธ ๋ฐ์ดํฐ๋ฅผ Tokenizer, Filter, Analyzer ๋ฑ์ ํ์ฉํ์ฌ ์ ์ฅํ๋ฉฐ, ํํ์ ๋ถ์ ๋ฐฉ์์ _analyze API๋ฅผ ํตํด ํ์ธํ ์ ์์ต๋๋ค. ์ด ๊ณผ์ ์์ ๋ฌธ์ฅ์ ์๋ณธ ๊ทธ๋๋ก ์ ์ฅ๋จ๊ณผ ๋์์, ํ ์คํธ ํ์ ์ ์ญ์์ธ(Inverted Index)์ด๋ผ๋ ํน๋ณํ ์๋ฃ๊ตฌ์กฐ์ ์ ์ฅ๋ฉ๋๋ค.
์ญ์์ธ(Inverted Index) ์์
Elasticsearch๋ ๊ฒ์์ ๋น ๋ฅด๊ฒ ์ํํ๊ธฐ ์ํด ์ญ์์ธ ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค. ์ญ์์ธ์ ํน์ ํ ํ ํฐ(๋จ์ด)์ด ์ด๋ค ๋ฌธ์์ ์ฐ๊ด๋์ด ์๋์ง๋ฅผ ์ ์ฅํ๋ ์๋ฃ๊ตฌ์กฐ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ์๋์ ๊ฐ์ ํํ๋ก ์ ์ฅ๋ฉ๋๋ค.
Term | Id |
potato | 1,2 |
wedge | 1 |
better | 2 |
์ด๋ฌํ ๊ตฌ์กฐ ๋๋ถ์ ๊ฒ์ ์ ๋น ๋ฅด๊ฒ ๊ฒฐ๊ณผ๋ฅผ ์กฐํํ๊ณ , ์ง๊ณํ์ฌ ๋ฐํํ ์ ์์ต๋๋ค.
ElasticSearch + SpringBoot ์ธํ
gradle ์์กด์ฑ ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-data-search'
ElasticSearchConfig
package com.iruyeon.v1.config.common;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
@Configuration
@EnableElasticsearchRepositories(basePackages = "org.springframework.data.elasticsearch.repository")
public class ElasticSearchConfig extends ElasticsearchConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
}
}
ClientDocumentRepository
package com.iruyeon.v1.domain.search.repository;
import com.iruyeon.v1.domain.search.entity.ClientDocument;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ClientDocumentRepository extends ElasticsearchRepository<ClientDocument, String> {
}
ClientDocument
package com.iruyeon.v1.domain.search.entity;
import com.iruyeon.v1.domain.client.entity.Client;
import com.iruyeon.v1.domain.client.entity.Family;
import com.iruyeon.v1.domain.client.enums.MaritalStatus;
import com.iruyeon.v1.domain.common.enums.Status;
import com.iruyeon.v1.domain.member.enums.Gender;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.List;
import java.util.Set;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Document(indexName = "client")
public class ClientDocument {
@Id
private String id;
private String name;
private String phoneNumber;
private String address;
private int birthYear;
private MaritalStatus maritalStatus;
private String highSchool;
private String university;
private String eduDegree;
private String major;
private String property;
private String religion;
private String currentJob;
private String previousJob;
private String jobDetail;
private String info;
private String homeTown;
private Gender gender;
private Status status;
private int ageGapLower;
private int ageGapUpper;
private String interest;
private String idealType;
private String personality;
@Field(name = "has_child", type=FieldType.Boolean)
private Boolean hasChild;
@Field(type = FieldType.Keyword)
private List<String> images;
@Field(type = FieldType.Nested)
private Set<Family> family;
@Builder
public ClientDocument(String id, String name, String phoneNumber, String address, int birthYear, String highSchool,
String university, String major, String property, String religion, String currentJob,
String previousJob, String jobDetail, String info, String homeTown, Gender gender, Status status,
int ageGapLower, int ageGapUpper, String interest, Set<Family> family, MaritalStatus maritalStatus,
String idealType, String personality, boolean hasChild, List<String> images, String eduDegree) {
this.id = id;
this.name = name;
this.phoneNumber = phoneNumber;
this.address = address;
this.birthYear = birthYear;
this.maritalStatus = maritalStatus;
this.highSchool = highSchool;
this.university = university;
this.major = major;
this.property = property;
this.religion = religion;
this.currentJob = currentJob;
this.previousJob = previousJob;
this.jobDetail = jobDetail;
this.info = info;
this.homeTown = homeTown;
this.gender = gender;
this.status = status;
this.ageGapLower = ageGapLower;
this.ageGapUpper = ageGapUpper;
this.interest = interest;
this.idealType = idealType;
this.personality = personality;
this.hasChild = hasChild;
this.images = images;
this.family = family;
this.eduDegree = eduDegree;
}
public static ClientDocument entityToDocument(Client client) {
return ClientDocument.builder()
.name(client.getName())
.phoneNumber(client.getPhoneNumber())
.address(client.getAddress())
.birthYear(client.getBirthYear())
.highSchool(client.getHighSchool())
.university(client.getUniversity())
.major(client.getMajor())
.property(client.getProperty())
.currentJob(client.getCurrentJob())
.previousJob(client.getPreviousJob())
.maritalStatus(client.getMaritalStatus())
.jobDetail(client.getJobDetail())
.info(client.getInfo())
.homeTown(client.getHomeTown())
.gender(client.getGender())
.status(client.getStatus())
.ageGapLower(client.getAgeGapLower())
.ageGapUpper(client.getAgeGapHigher())
.interest(client.getInterest())
.idealType(client.getIdealType())
.personality(client.getPersonality())
.hasChild(client.getHasChild())
.images(client.getImages())
.build();
}
}
ClientSearchService
package com.iruyeon.v1.domain.search.service;
import com.iruyeon.v1.domain.client.dto.ClientRequestDTO;
import com.iruyeon.v1.domain.search.entity.ClientDocument;
import com.iruyeon.v1.domain.search.repository.ClientDocumentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ClientSearchService {
private final ElasticsearchOperations operations;
private final ClientDocumentRepository clientDocumentRepository;
public ClientDocument saveClient(ClientDocument document) {
return clientDocumentRepository.save(document);
}
public List<ClientDocument> searchClient(ClientRequestDTO dto, int page) {
// ํ์ด์ง ์ค์
Pageable pageable = PageRequest.of(page, 10);
// ๊ฒ์ ์กฐ๊ฑด ์ค์
Criteria criteria = createConditionCriteria(dto);
Query query = new CriteriaQuery(criteria).setPageable(pageable);
// Elasticsearch ๊ฒ์ ์คํ
SearchHits<ClientDocument> searchHits = operations.search(query, ClientDocument.class);
// ๊ฒ์ ๊ฒฐ๊ณผ ๋ณํ ํ ๋ฐํ
return searchHits.getSearchHits()
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
private Criteria createConditionCriteria(ClientRequestDTO dto) {
Criteria criteria = new Criteria();
if (dto == null) {
return criteria;
}
if (dto.getCurrentJob() != null) {
criteria = criteria.and(Criteria.where("currentJob").is(dto.getCurrentJob()));
}
if (dto.getAgeGapHigher() > 0) {
criteria = criteria.and(Criteria.where("maxAge").greaterThanEqual(dto.getAgeGapHigher())); // โ
์์
}
if (dto.getAgeGapLower() > 0) {
criteria = criteria.and(Criteria.where("minAge").lessThanEqual(dto.getAgeGapLower())); // โ
์์
}
if (dto.getHasChild() != null) {
criteria = criteria.and(Criteria.where("hasChild").is(dto.getHasChild()));
}
if (dto.getMaritalStatus() != null) {
criteria = criteria.and(Criteria.where("maritalStatus").is(dto.getMaritalStatus()));
}
if (dto.getReligion() != null) {
criteria = criteria.and(Criteria.where("religion").is(dto.getReligion()));
}
if (dto.getUniversity() != null) {
criteria = criteria.and(Criteria.where("university").is(dto.getUniversity()));
}
return criteria;
}
}
- operations.search(query, ClientDocument.class) ๋์ ๋ฐฉ์
- query๋ CriteriaQuery ๊ธฐ๋ฐ์ ๊ฒ์ ์กฐ๊ฑด์ด ์ค์ ๋ ์ฟผ๋ฆฌ ๊ฐ์ฒด
- ClientDocument.class๋ ๊ฒ์ ๋์ ์ํฐํฐ ํ์ ์ ์ง์ ํฉ๋๋ค.
- Elasticsearch์ HTTP ์์ฒญ์ ๋ณด๋ด๊ณ , ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ JSON ํํ๋ก ์๋ตํ์ง๋ง, Spring Data Elasticsearch๋ ์ด๋ฅผ SearchHits<T> ๊ฐ์ฒด๋ก ๋ณํํฉ๋๋ค.
-
- SearchHits<ClientDocument>๋ ๋ค์ ์ ๋ณด๋ฅผ ํฌํจํ๊ณ ์์ต๋๋ค:
- ์ด ๊ฒ์ ๊ฒฐ๊ณผ ๊ฐ์ (getTotalHits())
- ๊ฐ ๊ฒ์ ๊ฒฐ๊ณผ์ ์ ์ (Elasticsearch Relevance Score)
- ๊ฒ์๋ ClientDocument ๋ฐ์ดํฐ (getContent())
- SearchHits<ClientDocument>๋ ๋ค์ ์ ๋ณด๋ฅผ ํฌํจํ๊ณ ์์ต๋๋ค:
index์ ํด๋นํ๋ ์ ์ฒด ๋ฐ์ดํฐ ์กฐํ
http://localhost:9200/client/_search?pretty=true
์กฐ๊ฑด์ ๋ง๋ ๋ฐ์ดํฐ ์กฐํ
Elasticsearch์์ ์กฐ๊ฑด์ ๋ง๋ ๋ฌธ์๋ฅผ ์กฐํํ๊ณ , ํด๋น ๊ฒฐ๊ณผ๋ฅผ ์ฝ๊ฒ ํ์ด์ง ์ฒ๋ฆฌํ ๊ฒฐ๊ณผ๊ฐ์ ํ์ธํ ์ ์์ต๋๋ค.
[์ฐธ๊ณ ]
https://esbook.kimjmin.net/07-settings-and-mappings
7. ์ธ๋ฑ์ค ์ค์ ๊ณผ ๋งคํ - Settings & Mappings | Elastic ๊ฐ์ด๋๋ถ
์ด ๋ฌธ์์ ํ๊ฐ๋์ง ์์ ๋ฌด๋จ ๋ณต์ ๋ ๋ฐฐํฌ ๋ฐ ์ถํ์ ๊ธ์งํฉ๋๋ค. ๋ณธ ๋ฌธ์์ ๋ด์ฉ ๋ฐ ๋ํ ๋ฑ์ ์ธ์ฉํ๊ณ ์ ํ๋ ๊ฒฝ์ฐ ์ถ์ฒ๋ฅผ ๋ช ์ํ๊ณ ๊น์ข ๋ฏผ(kimjmin@gmail.com)์๊ฒ ์ฌ์ฉ ๋ด์ฉ์ ์๋ ค์ฃผ์๊ธฐ ๋ฐ๋
esbook.kimjmin.net
https://ksb-dev.tistory.com/324
์คํ๋ง๋ถํธ๋ก ์๋ผ์คํฑ์์น ์ฟผ๋ฆฌ ๋ ๋ฆฌ๊ธฐ
0. ๊ฐ์์คํ๋ง๋ถํธ๋ก ์๋ผ์คํฑ์์น์ ์ ๊ทผํด์ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ ค๋ณด๊ฒ ์ต๋๋ค.ํ๋ก์ ํธ ๊ตฌ์กฐ๋ ์๋์ ๊ฐ์ต๋๋ค.โโesโโโsrc โโmain โโjava โ โโcom โ โโexample โ โโes_springboot โ โ EsSpring
ksb-dev.tistory.com
https://velog.io/@yukina1418/๊ฒ์์์ง-Elasticsearch์-๋ํ์ฌ
๊ฒ์์์ง Elasticsearch์ ๋ํ์ฌ
Elasticsearch์ ๋ํด์ ์์ธํ๊ฒ ์์๋ณด์.
velog.io
https://sihyung92.oopy.io/database/elasticsearch/1
๊ฒ์์์ง์ ์ฐ์ด๋ ์๋ผ์คํฑ์์น Elasticsearch์ ๋ํด ์์๋ณด์!
Elasticsearch๋
sihyung92.oopy.io