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