Backend
home

2025-6-30 (월)

생성일
2025/06/30 00:34
태그
ThredLocal
ElasticSearch

ThreadLocal

여러 스레드가 동일한 객체를 쓰더라도, 각 스레드가 서로 독립적인 자신만의 데이터를 가지도록 해준다.

주요 사용처

1.
로그 추적
2.
인증/인가(로그인 사용자 정보) 임시 저장
3.
트랜잭션/커넥션 관리

ThreadLocal + 로그 추적기

ElasticSearch

ElasticSearch란?

•
ElasticSearch는 대용량 데이터를 실시간으로 저장, 검색, 분석할 수 있는 분산형 검색 엔진

인덱싱

•
문서를 특정 인덱스에 저장하는 과정
•
이 과정에서 역색인(Inverted Index) 구조를 만들어서 검색이 빠르게 가능
•
ElasticSearch에서는 인덱스가 MySQL의 테이블과 동일한 의미를 가짐

역색인이란?

•
원본 데이터

ElasticSearch 인덱싱의 실제 흐름

ElasticSearch 노드(node), 클러스터(cluster), 샤드(shard), 복제(replica)

•
노드란 서버 한 대를 의미
◦
모든 노드들은 Cluster로 하나의 논리적 묶음으로 동작
•
클러스터란 여러 대의 노드를 하나의 논리적인 그룹으로 묶은 것
•
샤드란 인덱스를 여러 개의 작은 조각으로 나눈 것
◦
샤드 개수는 한 번 생성하면 변경 불가, 복제 개수는 변경 가능
•
복제란 각 샤드의 사본

운영환경에서 ElasticSearch 검색엔진의 최소 권장 노드(서버) 수

•
ES(ElasticSearch)에서 만든 인덱스 매핑과 Spring에서 @Document 자바 클래스 구조는 반드시 데이터 타입과 필드명이 일치해야 함
•
ElasticSearch에서 기본적으로 사용하는 standard analyzer는
◦
문장을 단어 단위로 쪼개고,
◦
모든 단어를 소문자로 바꾸는,
◦
가장 기본적인 분석기(analyzer) 이다.
•
단어 단위로 쪼개서 검색을 하면 중간에 있는 단어도 검색 가능
•
prefix 쿼리는 “색인된 토큰(단어)”의 “앞부분(접두사)”만 찾는 쿼리
•
match: “포함” (부분일치/중간문자/끝글자도 잡을 수 있음, analyzer 영향 큼)

쿼리 종류

실습 - 검색은 ElasticSearch로 진행, CRUD는 MySQL에서

•
build.gradle 에 ElasticSearch 의존 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
Shell
복사
•
application-properties
•
ElasticSearch 서비스 위한 controller, dto, repository, service 생성
◦
dto
package org.example.backendproject.board.elasticsearch.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.example.backendproject.board.dto.BoardDTO; import org.springframework.data.elasticsearch.annotations.Document; @JsonIgnoreProperties(ignoreUnknown = true) @Document(indexName = "board-index") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class BoardEsDocument { // 엘라스틱서치에 적용될 문서를 자바 객체로 정의한 클래스 // 엘라스틱 전용 DTO @Id private String id; private String title; private String content; private String username; private Long userId; private String created_date; private String updated_date; // BoardDTO를 ElasticSearch 전용 DTO로 변환하는 정적 메서드 public static BoardEsDocument from(BoardDTO dto) { // BoardDTO를 받아서 ElasticSearch DTO로 변환한다. return BoardEsDocument.builder() .id(String.valueOf(dto.getId())) .title(dto.getTitle()) .content(dto.getContent()) .username(dto.getUsername()) .userId(dto.getUser_id()) .created_date(dto.getCreated_date() != null ? dto.getCreated_date().toString() : null) .updated_date(dto.getUpdated_date() != null ? dto.getUpdated_date().toString() : null) .build(); } }
Java
복사
◦
repository
package org.example.backendproject.board.elasticsearch.repository; import org.example.backendproject.board.elasticsearch.dto.BoardEsDocument; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; @Repository public interface BoardEsRepository extends ElasticsearchRepository<BoardEsDocument, String> { // 문서 Id로 데이터 삭제하는 쿼리메서드 void deleteById(String id); }
Java
복사
◦
service
package org.example.backendproject.board.elasticsearch.service; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery; import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery; import co.elastic.clients.elasticsearch._types.query_dsl.PrefixQuery; import co.elastic.clients.elasticsearch._types.query_dsl.Query; import co.elastic.clients.elasticsearch.core.BulkRequest; import co.elastic.clients.elasticsearch.core.BulkResponse; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; import co.elastic.clients.elasticsearch.core.search.Hit; import java.io.IOException; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.backendproject.board.elasticsearch.dto.BoardEsDocument; import org.example.backendproject.board.elasticsearch.repository.BoardEsRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor public class BoardEsService { // 엘라스틱서치에 명령을 전달하는 자바 API private final ElasticsearchClient client; private final BoardEsRepository repository; // 데이터 저장 메서드 public void save(BoardEsDocument document) { repository.save(document); } // 데이터 삭제 메서드 public void deleteById(String id) { repository.deleteById(id); } // 검색 키워드와 페이지 번호와 페이지 크기를 받아서 엘라스틱서치에서 검색하는 메서드 // 검색된 정보와 페이징 정보도 함께 반환하도록 하기 위해 page 객체를 사용하여 반환 public Page<BoardEsDocument> search(String keyword, int page, int size) { try { // 엘라스틱서치에서 페이징을 위한 시작 위치를 계산하는 변수 int from = page * size; // 엘라스틱서치에서 사용할 검색조건을 담는 객체 Query query; // 검색어가 없으면 모든 문서를 검색하는 matchAll 쿼리 if (keyword == null || keyword.isBlank()) { query = MatchAllQuery.of(m -> m)._toQuery(); // 전체 문서를 가져오는 쿼리를 생성하는 람다 함수 // MatchAllQuery 는 엘라스틱서치에서 조건 없이 모든 문서를 검색할 때 사용하는 쿼리 } else { // 검색어가 있을 때 // boolquery는 복수 조건을 조합할 때 사용하는 쿼리 // 이 쿼리 안에서 여러 개의 조건을 나열 // 예를 들어 "백엔드" 라는 키워드가 들어왔을 때 이 "백엔드" 키워드를 어떻게 분석해서 데이터를 보여줄 것인가를 작성 query = BoolQuery.of(b -> { // PrefixQuery는 해당 필드가 특정 단어로 시작하는지 검사하는 쿼리 // MatchQuery는 해당 단어가 포함되어 있는지 검사하는 쿼리 /** * must: 모두 일치해야 함(AND) * should: 하나라도 일치해야 함(OR) * must_not: 해당 조건을 만족하면 제외 * filter: must와 같지만 점수 계산 안함 (속도 빠름) */ // 접두어 글자 검색 b.should(PrefixQuery.of(p -> p.field("title").value(keyword))._toQuery()); // 접두어 글자 검색, PrefixQuery는 해당 필드가 특정 단어로 시작하는지 검사하는 쿼리 b.should(PrefixQuery.of(p -> p.field("content").value(keyword))._toQuery()); // 초성 (ㅈㅂ 입력 시 자바 관련 글 검색 가능) b.should(PrefixQuery.of(p -> p.field("title.chosung").value(keyword))._toQuery()); b.should(PrefixQuery.of(p -> p.field("content.chosung").value(keyword))._toQuery()); // 중간 글자 검색 (match만 가능), 대소문자 상관없이 검색 가능 b.should(MatchQuery.of(m -> m.field("title.ngram").query(keyword))._toQuery()); b.should(MatchQuery.of(m -> m.field("content.ngram").query(keyword))._toQuery()); // fuzziness: "AUTO"는 오타 허용 검색 기능을 자동으로 켜주는 설정 -> 유사도 계산을 매번 수행하기 때문에 느림 // 짧은 키워드에는 사용 xxx // 오타 허용 (오타허용은 match만 가능) if (keyword.length() >= 3){ b.should(MatchQuery.of(m -> m.field("title").query(keyword).fuzziness("AUTO"))._toQuery()); b.should(MatchQuery.of(m -> m.field("content").query(keyword).fuzziness("AUTO"))._toQuery()); } return b; })._toQuery(); } // SearchRequest는 엘라스틱서치에서 검색을 하기 위한 검색요청 객체 // 인덱스명, 페이징 정보, 쿼리를 포함한 검색 요청 SearchRequest request = SearchRequest.of(s -> s .index("board-index") .from(from) .size(size) .query(query) ); // SearchResponse는 엘라스틱서치의 검색 결과를 담고 있는 응답 객체 SearchResponse<BoardEsDocument> response = // 엘라스틱서치에 명령을 전달하는 자바 API 검색요청을 담아서 응답객체로 반환 client.search(request, BoardEsDocument.class); // 위 응답객체에서 받은 검색 결과 중 문서만 추출해서 리스트로 만듦 // Hit는 엘라스틱서치에서 검색된 문서 1개를 감싸고 있는 객체 List<BoardEsDocument> content = response.hits() // 엘라스틱 서치 응답에서 hits(문서 검색결과) 전체를 꺼냄 .hits() // 검색 결과 안에 개별 리스트를 가져옴 .stream() // 자바 stream api를 사용 .map(Hit::source) // 각 Hit 객체에서 실제 문서를 꺼내는 작업 .collect(Collectors.toList()); // 위에서 꺼낸 객체를 자바 LIST에 넣음 // 전체 검색 결과 수 (총 문서의 갯수) long total = response.hits().total().value(); // PageImpl 객체를 사용하여 Spring 에서 사용할 수 있는 page 객체로 변환 return new PageImpl<>(content, PageRequest.of(page, size), total); } catch (IOException e) { log.error("검색 오류 ", e.getMessage()); throw new RuntimeException("검색 중 오류 발생", e); } } // 문서 리스트를 받아서 엘라스틱서치에 bulk 색인하는 메서드 public void bulkIndexInsert(List<BoardEsDocument> documents) throws IOException { // 한 번에 처리할 묶음(batch) 크기를 설정 int batchSize = 1000; for (int i = 0; i < documents.size(); i++) { // 현재 batch 의 끝 인덱스를 구함 int end = Math.min(i + batchSize, documents.size()); // 현재 batch 단위의 문서 리스트를 잘라냄 List<BoardEsDocument> batch = documents.subList(i, end); // 엘라스틱서치의 bulk 요청을 담을 빌더 생성 BulkRequest.Builder br = new BulkRequest.Builder(); // 각 문서를 bulk 요청 안에 하나씩 담음 for (BoardEsDocument document : batch) { br.operations(op -> op // operations() 로 하나하나 문서를 담음 .index(idx -> idx // 인덱스에 문서를 저장하는 작업 .index("board-index") // 인덱스명 .id(String.valueOf(document.getId())) // 수동으로 Id 지정 .document(document) // 실제 저장할 문서 객체 ) ); } // bulk 요청 실행 : batch 단위로 엘라스틱서치에 색인 수행 BulkResponse response = client.bulk(br.build()); // 벌크 작업 중 에러가 있는 경우 로그 출력 if (response.errors()) { for (BulkResponseItem item : response.items()) { if (item.error() != null) { // 실패한 문서의 ID와 에러 내용을 출력 log.error("엘라스틱서치 벌크 색인 작업 중 오류 발생 ID: {}, 오류 : {}", item.id(), item.error()); } } } } } }
Java
복사
◦
BoardService 쪽에 ElasticSearch 관련 기능 적용
/** 글 등록 **/ @Transactional public BoardDTO createBoard(BoardDTO boardDTO) { // userId(PK)를 이용해서 User 조회 if (boardDTO.getUser_id() == null) { throw new IllegalArgumentException("userId(PK)가 필요합니다!"); } // 연관관계 매핑! // 작성자 User 엔티티 조회 (userId 필요) User user = userRepository.findById(boardDTO.getUser_id()) .orElseThrow(() -> new IllegalArgumentException("작성자 정보가 올바르지 않습니다.")); // mysql 저장 Board board = new Board(); board.setTitle(boardDTO.getTitle()); board.setContent(boardDTO.getContent()); // 연관관계 매핑 board.setUser(user); Board saved = boardRepository.save(board); // mysql 저장 완료 // 엘라스틱서치에 저장 시작 - 추가 BoardEsDocument doc = BoardEsDocument.builder() .id(String.valueOf(board.getId())) .title(board.getTitle()) .content(board.getContent()) .userId(board.getUser().getId()) .username(board.getUser().getUserProfile().getUsername()) .created_date(String.valueOf(board.getCreated_date())) .updated_date(String.valueOf(board.getUpdated_date())) .build(); boardEsService.save(doc); return toDTO(saved); } /** 게시글 수정 **/ @Transactional public BoardDTO updateBoard(Long boardId, BoardDTO dto) { Board board = boardRepository.findById(boardId) .orElseThrow(() -> new IllegalArgumentException("게시글 없음: " + boardId)); board.setTitle(dto.getTitle()); board.setContent(dto.getContent()); boardRepository.save(board); // 엘라스틱서치에 데이터 수정 - 추가 BoardEsDocument doc = BoardEsDocument.builder() .id(String.valueOf(board.getId())) .title(board.getTitle()) .content(board.getContent()) .userId(board.getUser().getId()) .username(board.getUser().getUserProfile().getUsername()) .created_date(String.valueOf(board.getCreated_date())) .updated_date(String.valueOf(board.getUpdated_date())) .build(); boardEsService.save(doc); return toDTO(board); } /** 게시글 삭제 **/ @Transactional public void deleteBoard(Long userId, Long boardId) { Board board = boardRepository.findById(boardId) .orElseThrow(() -> new IllegalArgumentException("사용자 정보가 없습니다")); if (!board.getUser().getId().equals(userId)) { throw new IllegalArgumentException("삭제 권한이 없습니다."); } if (!boardRepository.existsById(boardId)) { throw new IllegalArgumentException("게시글 없음: " + boardId); } // mysql 삭제 boardRepository.deleteById(boardId); // 엘라스틱서치 삭제 - 추가 boardEsService.deleteById(String.valueOf(boardId)); } /** 배치 작업 (JdbcTemplate) **/ @Transactional public void batchSaveBoard(List<BoardDTO> boardDTOList) { Long start = System.currentTimeMillis(); int batchsize = 1000; // 한번에 처리할 배치 크기 for (int i = 0; i < boardDTOList.size(); i += batchsize) { // i는 1000씩 증가 // 전체 데이터를 1000개씩 잘라서 배치리스트에 담는다. int end = Math.min(boardDTOList.size(), i + batchsize); // 두 개의 숫자 중 작은 수를 반환 List<BoardDTO> batchList = boardDTOList.subList(i, end); // 전체 데이터에서 1000씩 작업을 하는데 마지막 데이터가 1000개가 안 될수도 있으니 // Math.min()으로 전체 크기를 넘지 않게 마지막 인덱스를 계산해서 작업한다. // 내가 넣은 데이터만 엘라스틱서치에 동기화하기 위해 uuid 생성 String batchKey = UUID.randomUUID().toString(); for (BoardDTO dto : batchList) { dto.setBatchKey(batchKey); } // 1. MySQL로 INSERT - 추가 batchRepository.batchInsert(batchList); // 2. MySQL에 Insert한 데이터를 다시 조회 - 추가 List<BoardDTO> saveBoards = batchRepository.findByBatchKey(batchKey); // 3. 엘라스틱서치용으로 변환 - 추가 List<BoardEsDocument> documents = saveBoards.stream() .map(BoardEsDocument::from) // DTO -> 엘라스틱서치용 dto로 변환 .toList(); try { // 4. 엘라스틱서치 bulk 인덱싱 - 추가 boardEsService.bulkIndexInsert(documents); } catch (IOException e) { log.error("[BOARD][BATCH] ElasticSearch 벌크 인덱싱 실패: {}", e.getMessage(), e); } } Long end = System.currentTimeMillis(); log.info("[BOARD][BATCH] 전체 저장 소요 시간(ms): {}", (end - start)); }
Java
복사
◦
controller
package org.example.backendproject.board.elasticsearch.controller; import lombok.RequiredArgsConstructor; import org.example.backendproject.board.elasticsearch.dto.BoardEsDocument; import org.example.backendproject.board.elasticsearch.service.BoardEsService; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController @RequestMapping("/boards") public class BoardEsController { private final BoardEsService boardEsService; @GetMapping("/elasticsearch") // 엘라스틱서치 검색 결과를 page 형태로 감싼 다음 HTTP 응답을 json으로 반환 public ResponseEntity<Page<BoardEsDocument>> elasticSearch( @RequestParam String keyword, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { return ResponseEntity.ok(boardEsService.search(keyword, page, size)); } }
Java
복사
◦
BatchRepository 쪽에 ElasticSearch 관련 코드 추가
public List<BoardDTO> findByBatchKey(String batchKey) { String sql = "SELECT b.id, b.title, b.content, b.user_id, b.created_date, b.updated_date, b.batch_key, up.username " + "FROM board b " + "JOIN user u ON b.user_id = u.id " + "JOIN user_profile up ON up.user_id = u.id " + "WHERE b.batch_key = ?"; return jdbcTemplate.query(sql, new Object[]{batchKey}, (rs, rowNum) -> { BoardDTO dto = new BoardDTO(); dto.setId(rs.getLong("id")); dto.setTitle(rs.getString("title")); dto.setContent(rs.getString("content")); dto.setUser_id(rs.getLong("user_id")); dto.setCreated_date(rs.getTimestamp("created_date") != null ? rs.getTimestamp("created_date").toLocalDateTime() : null); dto.setUpdated_date(rs.getTimestamp("updated_date") != null ? rs.getTimestamp("updated_date").toLocalDateTime() : null); dto.setBatchKey(rs.getString("batch_key")); dto.setUsername(rs.getString("username")); return dto; }); }
Java
복사
•
Dev Tools 메뉴에 접속하여 board-index 복사 후 붙여넣기
PUT /board-index { "settings": { //"number_of_shards": 3, //샤드 3개 //"number_of_replicas": 1, //복제본 1개 "index.max_ngram_diff": 3, //중간 문자 검색을 위해 단어를 쪼갤때 최대치와 최소치의 간극 정의 "analysis": { // 문장 분석기 설정 영역 "filter": { // 🔹 [1]"filter 정의 영역" (단어 단위로 잘라낸 후 그 단어들을 어떻게 변형할지 정의하는 영역) "autocomplete_filter": { // 내가 정의한 filter 이름 "type": "edge_ngram", // 앞에서부터 잘게 자름 예) 예: "spring" → "s", "sp", "spr", "spri", "sprin", "spring" "min_gram": 1, // 최소 몇 글자부터 자를지 "max_gram": 20 // 최대 몇 글자까지 자를지 }, //중간 문자 검색 "ngram_filter": { "type": "ngram", // 단어를 2~5글자씩 잘라서 부분 문자열을 만듬 (아래 정한 수치의 글자수 만큼) "min_gram": 2, // 예) "spring" -> 2글자 : sp, pr, ri, in, ng 3글자 : spr, pri, rin, ing "max_gram": 5 }, //초성 "chosung_filter": { "type": "hanhinsam_chosung" } }, "analyzer": { // 실제로 문장을 분석할 때 사용할 분석기를 정의하는 곳 (위에서 정의할 필터를 사용하는 곳) //접두어 검색 "autocomplete_analyzer": { // 내가 정의한 커스텀 분석기 이름 "type": "custom", // 내가 만든 커스텀 분석기 사용 "tokenizer": "standard", // 단어 단위로 분리 (공백, 특수문자 등 기준으로 단어 분리) -> ppt에 종류 있음 "filter": [ // 🔸 [2] "어떤 필터들을 사용할지 나열" "lowercase", // 소문자로 통일 "autocomplete_filter" // 위에서 정의한 필터를 사용 (접두사 조각 생성) 예) 예: "spring" → "s", "sp", "spr", "spri", "sprin", "spring" ] }, //중간 문자열 검색 "ngram_analyzer": { "type": "custom", "tokenizer": "standard", "filter": [ "lowercase", "ngram_filter" ] }, //초성 검색 "chosung_analyzer": { "type": "custom", "tokenizer": "standard", "filter": [ "chosung_filter" ] } } } }, "mappings": { "properties": { "_class": { //spring data elasticsearch에서 객체를 저장할 때 자동으로 추가하는 메타데이터 "type": "keyword", "index": false, // 🔒 저장은 되지만 검색/정렬에서 제외 "doc_values": false // 🔒 집계에도 사용 안 함 }, "content": { "type": "text", // ✅ 자연어 문장을 검색할 수 있는 필드라는 의미 -> ppt에 종류 있음 "analyzer": "autocomplete_analyzer", // ✅ (인덱싱용) 텍스트를 어떻게 쪼개서 색인(저장)할지 (위 settings -> analysis -> analyzer 에서 만든 분석기 사용) "search_analyzer": "standard", // ✅ (검색용) 검색 시에는 어떻게 분석해서 찾을지 -> ppt에 종류 있음 //서브 필드 (서브 필드는 쿼리에서 명시적으로 필드명을 지정할 때만 적용이 됨) "fields": { //여러 방식으로 색인/검색하고 싶을 때 추가로 사용하는 서브필드 "keyword": { //정확히 일치하는 값으로 검색 "type": "keyword", "ignore_above": 256 // ✅ 256자 이상인 문자열은 인덱싱하지 않겠다는 설정 (일반적으로 Elasticsearch의 성능 보호를 위한 제한) }, "chosung": { // 초성 검색 전용 필드 "type": "text", "analyzer": "chosung_analyzer" // ✅ 초성검색 }, "ngram": { //중간 문자열 검색 "type": "text", "analyzer": "ngram_analyzer" } } }, "title": { "type": "text", "analyzer": "autocomplete_analyzer", "search_analyzer": "standard", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 }, "chosung": { // 초성 검색 전용 필드 "type": "text", "analyzer": "chosung_analyzer" }, "ngram": { "type": "text", "analyzer": "ngram_analyzer" } } }, "id": { "type": "text", // ⚠️ 원래는 keyword로 쓰는 게 더 좋음 (식별자니까) "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "userId": { "type": "long" // 사용자 ID (정수형 필터/정렬 가능) }, "username": { "type": "text", // 사용자 이름 (검색용) "fields": { "keyword": { "type": "keyword", "ignore_above": 256 // 정렬/집계용 } } }, "created_date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss.SSSSSS||yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis" //여러 개의 포맷 중 하나라도 맞으면 날짜로 인식 }, "updated_date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss.SSSSSS||yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis" } } } }
Shell
복사
•
초성 검색 관련 플러그인 다운 받기
◦
명령어 실행
docker cp hanhinsam-0.1.zip elasticsearch:/tmp/
docker exec -it elasticsearch /bin/bash
cd /usr/share/elasticsearch
./bin/elasticsearch-plugin install file:///tmp/hanhinsam-0.1.zip
~/Desktop/project/backend5 │ main +5 !260 ?37 cd ElasticSearch ✔ ~/Desktop/project/backend5/ElasticSearch │ main +5 !260 ?37 docker cp hanhinsam-0.1.zip elasticsearch:/tmp/ ✔ Successfully copied 15.9kB to elasticsearch:/tmp/ ~/Desktop/project/backend5/ElasticSearch │ main +5 !260 ?37 docker exec -it elasticsearch /bin/bash ✔ elasticsearch@de15fe939cdb:~$ cd /usr/share/elasticsearch elasticsearch@de15fe939cdb:~$ ./bin/elasticsearch-plugin install file:///tmp/hanhinsam-0.1.zip -> Installing file:///tmp/hanhinsam-0.1.zip -> Downloading file:///tmp/hanhinsam-0.1.zip [=================================================] 100%?? -> Installed hanhinsam -> Please restart Elasticsearch to activate any plugins installed elasticsearch@de15fe939cdb:~$
Shell
복사
•
Elasticsearch, board-index 생성
# Click the Variables button, above, to create your own variables. GET ${exampleVariable1} // _search { "query": { "${exampleVariable2}": {} // match_all } } PUT /board-index { "settings": { //"number_of_shards": 3, //샤드 3개 //"number_of_replicas": 1, //복제본 1개 "index.max_ngram_diff": 3, //중간 문자 검색을 위해 단어를 쪼갤때 최대치와 최소치의 간극 정의 "analysis": { // 문장 분석기 설정 영역 "filter": { // 🔹 [1]"filter 정의 영역" (단어 단위로 잘라낸 후 그 단어들을 어떻게 변형할지 정의하는 영역) "autocomplete_filter": { // 내가 정의한 filter 이름 "type": "edge_ngram", // 앞에서부터 잘게 자름 예) 예: "spring" → "s", "sp", "spr", "spri", "sprin", "spring" "min_gram": 1, // 최소 몇 글자부터 자를지 "max_gram": 20 // 최대 몇 글자까지 자를지 }, //중간 문자 검색 "ngram_filter": { "type": "ngram", // 단어를 2~5글자씩 잘라서 부분 문자열을 만듬 (아래 정한 수치의 글자수 만큼) "min_gram": 2, // 예) "spring" -> 2글자 : sp, pr, ri, in, ng 3글자 : spr, pri, rin, ing "max_gram": 5 }, //초성 "chosung_filter": { "type": "hanhinsam_chosung" } }, "analyzer": { // 실제로 문장을 분석할 때 사용할 분석기를 정의하는 곳 (위에서 정의할 필터를 사용하는 곳) //접두어 검색 "autocomplete_analyzer": { // 내가 정의한 커스텀 분석기 이름 "type": "custom", // 내가 만든 커스텀 분석기 사용 "tokenizer": "standard", // 단어 단위로 분리 (공백, 특수문자 등 기준으로 단어 분리) -> ppt에 종류 있음 "filter": [ // 🔸 [2] "어떤 필터들을 사용할지 나열" "lowercase", // 소문자로 통일 "autocomplete_filter" // 위에서 정의한 필터를 사용 (접두사 조각 생성) 예) 예: "spring" → "s", "sp", "spr", "spri", "sprin", "spring" ] }, //중간 문자열 검색 "ngram_analyzer": { "type": "custom", "tokenizer": "standard", "filter": [ "lowercase", "ngram_filter" ] }, //초성 검색 "chosung_analyzer": { "type": "custom", "tokenizer": "standard", "filter": [ "chosung_filter" ] } } } }, "mappings": { "properties": { "_class": { //spring data elasticsearch에서 객체를 저장할 때 자동으로 추가하는 메타데이터 "type": "keyword", "index": false, // 🔒 저장은 되지만 검색/정렬에서 제외 "doc_values": false // 🔒 집계에도 사용 안 함 }, "content": { "type": "text", // ✅ 자연어 문장을 검색할 수 있는 필드라는 의미 -> ppt에 종류 있음 "analyzer": "autocomplete_analyzer", // ✅ (인덱싱용) 텍스트를 어떻게 쪼개서 색인(저장)할지 (위 settings -> analysis -> analyzer 에서 만든 분석기 사용) "search_analyzer": "standard", // ✅ (검색용) 검색 시에는 어떻게 분석해서 찾을지 -> ppt에 종류 있음 //서브 필드 (서브 필드는 쿼리에서 명시적으로 필드명을 지정할 때만 적용이 됨) "fields": { //여러 방식으로 색인/검색하고 싶을 때 추가로 사용하는 서브필드 "keyword": { //정확히 일치하는 값으로 검색 "type": "keyword", "ignore_above": 256 // ✅ 256자 이상인 문자열은 인덱싱하지 않겠다는 설정 (일반적으로 Elasticsearch의 성능 보호를 위한 제한) }, "chosung": { // 초성 검색 전용 필드 "type": "text", "analyzer": "chosung_analyzer" // ✅ 초성검색 }, "ngram": { //중간 문자열 검색 "type": "text", "analyzer": "ngram_analyzer" } } }, "title": { "type": "text", "analyzer": "autocomplete_analyzer", "search_analyzer": "standard", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 }, "chosung": { // 초성 검색 전용 필드 "type": "text", "analyzer": "chosung_analyzer" }, "ngram": { "type": "text", "analyzer": "ngram_analyzer" } } }, "id": { "type": "text", // ⚠️ 원래는 keyword로 쓰는 게 더 좋음 (식별자니까) "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "userId": { "type": "long" // 사용자 ID (정수형 필터/정렬 가능) }, "username": { "type": "text", // 사용자 이름 (검색용) "fields": { "keyword": { "type": "keyword", "ignore_above": 256 // 정렬/집계용 } } }, "created_date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss.SSSSSS||yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis" //여러 개의 포맷 중 하나라도 맞으면 날짜로 인식 }, "updated_date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss.SSSSSS||yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis" } } } }
Shell
복사
•
플러그인 설치 내역 확인
•
플러그인 설치 후 PUT 쪽에서 재생 버튼 누르면 다음과 같이 출력됨
•
1000개 이상의 데이터, 배치 등록
•
ElasticSearch 인덱스 조회 결과 확인
{ "took": 3, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 7004, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "board-index", "_id": "16228", "_score": 1, "_source": { "id": "16228", "title": "몽고디비와 자바의 예제 204", "content": "JWT를 활용한 웹소켓 개발 및 쿠버네티스 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:24", "updated_date": "2023-03-18T13:24" } }, { "_index": "board-index", "_id": "16229", "_score": 1, "_source": { "id": "16229", "title": "머신러닝와 데이터베이스의 예제 205", "content": "뷰를 활용한 MySQL 개발 및 NoSQL 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:25", "updated_date": "2023-03-18T13:25" } }, { "_index": "board-index", "_id": "16230", "_score": 1, "_source": { "id": "16230", "title": "API와 JPA의 예제 206", "content": "뷰를 활용한 보안 개발 및 리눅스 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:26", "updated_date": "2023-03-18T13:26" } }, { "_index": "board-index", "_id": "16231", "_score": 1, "_source": { "id": "16231", "title": "REST와 웹소켓의 예제 207", "content": "JWT를 활용한 몽고디비 개발 및 백엔드 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:27", "updated_date": "2023-03-18T13:27" } }, { "_index": "board-index", "_id": "16232", "_score": 1, "_source": { "id": "16232", "title": "JPA와 API의 예제 208", "content": "API를 활용한 데이터베이스 개발 및 보안 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:28", "updated_date": "2023-03-18T13:28" } }, { "_index": "board-index", "_id": "16233", "_score": 1, "_source": { "id": "16233", "title": "리액트와 딥러닝의 예제 209", "content": "데이터베이스를 활용한 JPA 개발 및 보안 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:29", "updated_date": "2023-03-18T13:29" } }, { "_index": "board-index", "_id": "16234", "_score": 1, "_source": { "id": "16234", "title": "JPA와 NoSQL의 예제 210", "content": "AI를 활용한 뷰 개발 및 엘라스틱서치 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:30", "updated_date": "2023-03-18T13:30" } }, { "_index": "board-index", "_id": "16235", "_score": 1, "_source": { "id": "16235", "title": "프론트엔드와 도커의 예제 211", "content": "쿠버네티스를 활용한 웹소켓 개발 및 리눅스 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:31", "updated_date": "2023-03-18T13:31" } }, { "_index": "board-index", "_id": "16236", "_score": 1, "_source": { "id": "16236", "title": "리액트와 웹소켓의 예제 212", "content": "REST를 활용한 마이크로서비스 개발 및 MySQL 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:32", "updated_date": "2023-03-18T13:32" } }, { "_index": "board-index", "_id": "16237", "_score": 1, "_source": { "id": "16237", "title": "리눅스와 쿠버네티스의 예제 213", "content": "파이썬를 활용한 리눅스 개발 및 쿠버네티스 연동 방법에 대해 설명합니다.", "username": "hms", "userId": 1, "created_date": "2023-03-16T13:33", "updated_date": "2023-03-18T13:33" } } ] } }
JSON
복사
•
초성 입력
•
대소문자 구분없이 조회 가능
•
오타를 입력해도 조회 가능

오늘 푸시한 커밋리스트

날짜
커밋 메시지
2025-06-30
2025-06-30
2025-06-30
2025-06-30
2025-06-30
2025-06-30
2025-06-30
2025-06-30
2025-06-30
2025-06-30
2025-06-30