(멋쟁이사자처럼_백엔드스쿨플러스) Day16 엘라스틱 서치
스프링으로 ElasticSearch 연동하기
kibana 설치 및 시각화 도구 사용하기
Docker compose를 이용하여 kibana를 설치한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
servies: kibana: image: docker.elastic.co/kibana/kibana:8.3.3 container_name: kibana environment: SERVER_NAME: kibana ELASTICSEARCH_HOSTS: http://elasticsearch:9200 ports: - 5601:5601 depends_on: - elasticsearch networks: - elastic networks: elastic: driver: bridge
dev-tools를 이용하여 데이터 확인하기
post-man을 통해 확인 한 api 요청을 바로 확인할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
# title 검색 GET /app1_posts/_search { "query": { "match": { "title": "검색어" } } } # content 검색 GET /app1_posts/_search { "query": { "match": { "content": "검색어" } } } # title, content 모두 검색 GET /app1_posts/_search { "query": { "multi_match": { "query": "여행", "fields": ["title", "content"] } } } # 부분일치 GET /app1_posts/_search { "query": { "wildcard": { "title": "*출근*" } } } # 여러조건을 조합 GET /app1_posts/_search { "query": { "bool": { "must": [ { "match": { "title": "코딩" } }, { "match": { "content": "오류" } } ], "filter": [ { "term": { "_index": "app1_posts" } } ] } } } # 정렬 GET /app1_posts/_search { "query": { "match_all": {} }, "sort": [ { "id": "asc" } ] } #제한 GET /app1_posts/_search { "query": { "match_all": {} }, "size": 5 }
sql문을 사용하여 데이터를 조회할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
# 모든 문서 조회 POST /_sql?format=txt { "query": "SELECT * FROM app1_posts" } # 특정 컬럼만 조회 POST /_sql?format=txt { "query": "SELECT title, content FROM app1_posts" } # 조건 조회 # title에 특정 단어가 포함된 문서 조회: POST /_sql?format=txt { "query": "SELECT * FROM app1_posts WHERE title LIKE '%검색어%'" } # 정렬 # views 필드 기준 내림차순 정렬: POST /_sql?format=txt { "query": "SELECT * FROM app1_posts ORDER BY views DESC" } # 결과 제한 # 최대 5개의 결과만 조회: POST /_sql?format=txt { "query": "SELECT * FROM app1_posts LIMIT 5" } # 집계 # views 필드의 평균값 계산: POST /_sql?format=txt { "query": "SELECT AVG(views) AS avg_views FROM app1_posts" } # 날짜 조건 추가 # published_date가 2025년 1월 1일 이후인 문서 검색: POST /_sql?format=txt { "query": "SELECT * FROM app1_posts WHERE published_date >= '2025-01-01'" } # SQL 쿼리를 JSON 형식으로 반환 # 결과를 JSON 형식으로 얻고 싶다면 format=txt 대신 format=json을 사용합니다. POST /_sql?format=json { "query": "SELECT * FROM app1_posts" }
스프링으로 ElasticSearch 연동하기
PostDocs 수정
@Field(type = FieldType.Text)
필드 매핑을 통해 text로 매핑한다.필드 매핑 정의
- 자바 클래스의 필드를 Elasticsearch의 필드 타입으로 매핑
- FieldType.Text는 이 필드가 Elasticsearch에서 text 타입으로 저장
Text 타입의 특징
- 전문 검색(Full-text search)이 가능
- 필드의 내용이 분석(analyze)되어 검색을 위한 토큰으로 분리
- ex) “Hello World”라는 내용은 “hello”, “world” 두 개의 토큰으로 분리되어 저장
1
2
3
4
5
6
7
8
public class PostDoc {
@Id
private String id;
@Field(type = FieldType.Text)
private String title;
@Field(type = FieldType.Text)
private String content;
}
기존 controller를 이용하여 검색기능 추가
- 데이터 추가
- 데이터 추가는 POST요청으로 데이터를 추가한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@PostMapping("/write") public RsData<PostDoc> write( @RequestBody @Valid PostDocWriteRequest writeRequest) { PostDoc postDoc = postDocService.write(writeRequest.title, writeRequest.content); return new RsData<>( "200", "게시글이 생성됨", postDoc ); } record PostDocWriteRequest( @NotBlank String title, @NotBlank String content ) {}
- 데이터 조회
- 데이터 조회는 GET요청으로 데이터를 조회한다.
1 2 3 4 5
@GetMapping("/search") public List<PostDoc> search( @RequestParam("keyworad") String keyword) { return postDocService.findByTitle(keyword); }
Service 수정
- 기존 write메서드는 동일하게 repository에 ElasticSearchRepository를 상속받아 데이터를 save한다.
조회 코드는 findByTitle메서드를 추가하여 title로 검색하는 메서드를 추가한다.
1 2 3
public List<PostDoc> findByTitle(String keyword) { return postDocRepository.findByTitleContainingOrContentContaining(keyword, keyword); }
PostDocRepository 수정
itleContainingOrContentContaining매서드는 기본 메서드가 아니기 때문에 추상매서드로 작성한다.
1 2 3
public interface PostDocRepository extends ElasticsearchRepository<PostDoc, String> { List<PostDoc> findByTitleContainingOrContentContaining(String keyword, String keyword2); }
메서드 이름 분석
- findBy: 검색을 수행하라는 명령
- TitleContaining: title 필드에서 포함된 내용을 찾음
- Or: 또는
- ContentContaining: content 필드에서 포함된 내용을 찾음
postman을 이용하여 검색기능 확인
MySQL과 ElasticSearch 동시 사용하기
- 새로은 Post 도메인을 만들어 sql 방식의 controller, service, entity, repository를 만든다.
- Jpa를 이용하여 SQL을 관리한다.
- 레포
참고. AOP를 이용한 Response 수정
- AOP(Aspect-Oriented Programming)는 공통 관심사(Cross-cutting concerns)를 분리해서 관리하는 프로그래밍 패러다임이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 기존 컨트롤러 코드
// 모든 컨트롤러에서 이런 코드를 반복해서 작성해야 합니다
@GetMapping("/posts")
public RsData<List<Post>> getPosts() {
List<Post> posts = postService.getPosts();
RsData<List<Post>> rsData = RsData.of("S-1", "성공", posts);
response.setStatus(rsData.getStatusCode()); // 이 부분이 모든 컨트롤러에서 반복됨
return rsData;
}
// AOP를 이용한 코드
@GetMapping("/posts")
public RsData<List<Post>> getPosts() {
List<Post> posts = postService.getPosts();
return RsData.of("S-1", "성공", posts); // 상태 코드 설정 걱정 없이 RsData만 반환
}
- AOP를 사용하면
- 모든 컨트롤러에서 상태 코드 설정하는 중복 코드를 제거
- 컨트롤러는 비즈니스 로직에만 집중
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Aspect // AOP 클래스임을 나타냄
@Component
@RequiredArgsConstructor
public class ResponseAspect {
private final HttpServletResponse response;
// @Around: 메서드 실행 전후에 처리
@Around("""
(
within
(
@org.springframework.web.bind.annotation.RestController *
)
&&
(
@annotation(org.springframework.web.bind.annotation.GetMapping)
||
@annotation(org.springframework.web.bind.annotation.PostMapping)
||
@annotation(org.springframework.web.bind.annotation.PutMapping)
||
@annotation(org.springframework.web.bind.annotation.DeleteMapping)
||
@annotation(org.springframework.web.bind.annotation.RequestMapping)
)
)
||
@annotation(org.springframework.web.bind.annotation.ResponseBody)
""")
public Object handleResponse(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 컨트롤러 메서드 실행
Object proceed = joinPoint.proceed();
// 2. 컨트롤러에서 반환된 값으로 후처리
if (proceed instanceof RsData<?>) {
RsData<?> rsData = (RsData<?>) proceed;
response.setStatus(rsData.getStatusCode());
}
// 3. MessageConverter로 전달
return proceed;
}
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.