GCP에 ElasticSearch 띄워서 검색 기능 구현하기

2024. 12. 4. 22:02·Dev

intro

앱 기능 중 음식명으로 레시피를 조회하는 기능이 있습니다. 이 검색 기능에는 ElasticSearch를 사용해서 구현했습니다. ElasticSearch를 사용하면 성능이 매우 좋다는 정보를 많이 접했기 때문에 이를 기반으로 구현해 보았습니다.

elasticsearch를 사용한 이유

엘라스틱서치를 검색 엔진으로 사용할 때 독보적인 성능을 가지고 있습니다. 그 이유는 데이터를 저장하는 방식이 rdb와는 다른 방식을 사용해서 저장을 하고 있습니다.
엘라스틱 서치에서는 색인이라고 표현하는데 rdb에서 인덱스라고 표현하는 것을 역인덱스 구조로 저장하는 방식입니다.
키워드에 인덱스를 주어지게 되어서 검색할 때 키워드를 이용해서 찾기 때문에 빠른 검색을 할 수 있습니다.

Tool

  • Spring 2.7.7
  • ElasticSearch 7.17.3
  • Google Cloud Service
  • GCP에서 무료 크레딧을 제공해주어서 GCP를 이용해서 서버를 구축해 보았습니다.*

 

원격 접속해서 docker를 이용해서 ElasticSearch 서버를 구축했습니다

ssh 키 만들어주기

$ ssh-keygen -t rsa -f ~/.ssh/[키이름] -C [gmail계정] -b 4096

 

생성된 pub 파일 내용을 메타 데이터에 ssh키 등록해주기

ssh -i [private 키파일경로] [계정]@[외부IP]

레시피 db에 저장하기

만개의 레시피 데이터를 json 파일로 db에 저장하기 쉽게 만들어 두었습니다.

  • 일단 local에서 테스트 하기 위해 데이터를 db에 저장해주었습니다.

GCP에 elasticsearch 서버 구축하기

위에서 생성한 GCP 인스턴스에 원격접속해서 docker를 이용해서 elasticsearch 컨테이너를 실행해주었습니다.

 

도커 설치

//설치
sudo apt update
sudo apt install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io

//실행
sudo systemctl enable docker
sudo service docker start

//확인
sudo service docker status

 

elasticsearch, kibana 설치

//자신에게 맞는 버전 설치
//elasticsearch
sudo docker pull docker.elastic.co/elasticsearch/elasticsearch:7.17.3
sudo docker volume create elasticsearch-volume
sudo docker run -d -p 9200:9200 -e "discovery.type=single-node" --name elasticsearch docker.elastic.co/elasticsearch/elasticsearch:7.17.3
sudo docker ps

//kibana
sudo docker pull docker.elastic.co/kibana/kibana:7.17.3
sudo docker run -d --link elasticsearch:elasticsearch -p 5601:5601 docker.elastic.co/kibana/kibana:7.17.3
sudo docker ps

 

잘 작동하는지 확인하기 위해 gcp 9200포트와 5601포트를 열어보았습니다

 

 

Nori Tokenizer 설치
한글을 형태소 단위로 분석하여 검색을 할 수 있게 지원해주는 Nori tokenizer를 설치해 주었습니다.

sudo docker exec -it {elasticsearch 컨테이너 이름} /bin/bash
./bin/elasticsearch-plugin install analysis-nori

 

보안설정

//elasticsearch 컨테이너 접속
sudo docker exec -it elasticsearch /bin/bash
vi /usr/share/elasticsearch/config/elasticsearch.yml
//xpack.security.enabled: true 
//xpack.security.transport.ssl.enabled: true 추가 후

// /usr/share/elasticsearch/bin 폴더에서 아래 코드 실행후 비밀번호 설정
elasticsearch-setup-passwords interactive

//kibana 컨테이너 접속
sudo docker exec -it {kibana 커넽이너 이름} /bin/bash
vi config/kibana.yml
//elasticsearch.username: "elastic"
//elasticsearch.password: "비밀번호" 추가하기

 

암호가 생긴 것을 확인할 수 있습니다

 

 

spring batch로 수집한 데이터 ELK로 전송하기

주기적으로 db에 있는 레시피 데이터를 elasticsearch에 저장하기 위해서 spring batch를 사용했습니다.

@Slf4j
@Configuration
@EnableBatchProcessing
@AllArgsConstructor
public class ElasticBatchConfiguration {

    private final JobBuilderFactory jobBuilderFactory; 
    private final StepBuilderFactory stepBuilderFactory; 
    private final RecipeRepository recipeRepository;
    private final RecipeElasticRepository recipeDocumentRepository;

    @Bean
    public ItemReader<Recipe> reader() {
        LocalDateTime lastProcessedDate = getLastProcessedDate(); // 마지막 처리 시점 가져오기
        return new RepositoryItemReaderBuilder<Recipe>()
                .repository(recipeRepository)
                .methodName("findUpdatedAfter")
                .arguments(lastProcessedDate)
                .pageSize(200)
                .sorts(Collections.singletonMap("id", Sort.Direction.DESC))
                .name("recipeItemReader")
                .build();
    }

    @Bean
    public ItemProcessor<Recipe, RecipeDocument> processor() {

        return recipe -> RecipeDocument.builder()
                    .id(recipe.getId().toString())
                    .name(recipe.getName())
                    .info(recipe.getInfo())
                    .description(recipe.getDescription())
                    .ingredient(recipe.getIngredient())
                    .recommendCount(recipe.getRecommendCount())
                    .bookmark(recipe.getBookmark())
                    .build();

    }

    @Bean
    public ItemWriter<RecipeDocument> writer() {
        return items -> {
            log.info("Writing {} items to ElasticSearch", items.size());
            recipeDocumentRepository.saveAll(items);
        };
    }

    @Bean
    public TaskExecutor taskExecutor() {
        return new SimpleAsyncTaskExecutor("batch_task_executor");
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get("step1")
                .<Recipe, RecipeDocument>chunk(200)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .taskExecutor(taskExecutor()) // 병렬 처리
                .throttleLimit(4) 
                .build();
    }



    @Bean
    public Job importRecipeJob(JobRepository jobRepository) {
        return jobBuilderFactory.get("importRecipeJob")
                .incrementer(new RunIdIncrementer())
                .preventRestart()
                .flow(step1())
                .end()
                .build();
    }
}

 

elasticSearch에 데이터가 잘 저장된 것을 확인할 수 있습니다.



elasticSearch에 저장된 데이터 조회 api 만들기

 

elasticSearch 연결을 위한 config 추가

@Configuration
@EnableElasticsearchRepositories(basePackages = "com.sm.project.elasticsearch.repository")
public class ElasticConfig {

    @Value("${spring.elastic.url}")
    private String elasticUrl;

    @Value("${spring.elasticsearch.username}")
    private String username;

    @Value("${spring.elasticsearch.password}")
    private String password;

    @Bean
    public RestHighLevelClient elasticsearchClient() {
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo(elasticUrl)
                .withBasicAuth(username,password)
                .build();

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

}

RecipeDocument를 조회할 수 있는 Elasticsearch Repository를 작성합니다.

@Repository("elasticSearchRepository")
public interface RecipeElasticRepository extends ElasticsearchRepository<RecipeDocument, Long> {
    List<RecipeDocument> findByName(String name);
}

재료를 이용해서 검색했을때 추천수 기준으로 정렬해서 5개의 레시피를 보여주도록 로직을 작성합니다.

@Service
@AllArgsConstructor
@Transactional
public class RecipeService {

    private final RecipeElasticRepository recipeDocumentRepository;


    // 추천수 높은 순으로 페이징된 레시피를 조회하는 메소드
    public Page<RecipeDocument> findTopRecipes(int lastIndex, int limit) {
        Pageable pageable = PageRequest.of(lastIndex / limit, limit, Sort.by(Sort.Direction.DESC, "recommendCount"));
        return recipeDocumentRepository.findAllByOrderByRecommendCountDesc(pageable);
    }
    //검색한 재료로 레시피를 조회하는 메소드
    public Page<RecipeDocument> searchByIngredient(String ingredient, int lastIndex, int limit) {
        Pageable pageable = PageRequest.of(lastIndex / limit, limit, Sort.by(Sort.Direction.DESC, "recommendCount"));
        return recipeDocumentRepository.findByIngredientContainingOrderByRecommendCountDesc(ingredient, pageable);
    }
}

 

떡으로 검색한 결과가 잘 나온 것을 확인할 수 있습니다.

 

 

'Dev' 카테고리의 다른 글

상품 주문하기 동시성 문제 해결하기  (0) 2024.12.04
과도한 트래픽에 대한 방어하기  (1) 2024.12.04
위치 기반으로 글 조회 기능 구현  (1) 2024.12.04
fcm 이용해서 앱 푸쉬 구현  (0) 2024.12.04
소켓 통신(채팅방 구현)  (0) 2024.12.04
'Dev' 카테고리의 다른 글
  • 상품 주문하기 동시성 문제 해결하기
  • 과도한 트래픽에 대한 방어하기
  • 위치 기반으로 글 조회 기능 구현
  • fcm 이용해서 앱 푸쉬 구현
khjoon
khjoon
  • khjoon
    기록기록
    khjoon
  • 전체
    오늘
    어제
    • 분류 전체보기 (36)
      • Security (1)
      • Dev (14)
      • Infra (12)
      • Ops (9)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
khjoon
GCP에 ElasticSearch 띄워서 검색 기능 구현하기
상단으로

티스토리툴바