본문 바로가기
프로젝트

GiftFunding) Elasticsearch를 활용하여 상품검색 기능 구현

by son_i 2023. 12. 9.
728x90

해줘야 할 일은 크게 3가지

1. 인덱스 생성

2. 데이터 저장

3. 검색 API 요청

 

* Elasaticsearch의 Client

자바에서 ES API를 이용하기 위해서 ES에서 만들어 놓은 클라이언트가 필요함.

ES 7버전까지 Client는 HighLevelClient와 LowLevelClient가 있음.

HighLevelClient는 API를 추상화시켜 놓아서 SDK 형식으로 사용할 수 있음.

ES 8버전으로 넘어오면서 이 기능이 더이상 사용되지 않음.

 

Elasticsearch 연결

build.greadle에 아래의 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'

Elasticsearch 구성

application.properties에 ES관련 설정.

spring.data.elasticsearch.cluster-nodes=localhost:9300
spring.data.elasticsearch.cluster-name=docker-cluster

 

Config 작성

http port와 통신할 주소를 적어줌.

@Configuration
@EnableElasticsearchRepositories
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
    @Value("${elasticsearch.host}")
    private String host;

    @Value("${elasticsearch.port}")
    private int port;


    @Override
    public RestHighLevelClient elasticsearchClient() {
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
            .connectedTo(host+":"+port)
            .build();
        return RestClients.create(clientConfiguration).rest();
    }
}

 

 

Document 생성

Document로 관리할 클래스에 @Document를 달아줘야하는데 Entity와 Document를 따로 만들어야함.

Spring boot의 AutoConfiguration이 작동할 때 JPA와 Elastic search 중 Repository에 어떤 걸 사용할지 충돌이 발생하기 때문.

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Setting(settingPath = "/elastic/elastic-settings.json")
@Mapping(mappingPath = "/elastic/product-mappings.json")
@Document(indexName = "product")
public class ProductDocument {
    @Id
    private Long id;

    private String productName;
    private Long price;
    private int ranking;

    public static ProductDocument from(Product product) {
        return ProductDocument.builder()
            .id(product.getId())
            .productName(product.getProductName())
            .price(product.getPrice())
            .ranking(product.getRanking())
            .build();
    }
}

- ES에 타입을 매핑하는 방법 2가지

   1. @Field 어노테이션을 통해 직접 타입 설정. 

   2. @Setting, @Mapping 사용

       @Setting은 분석기를 매핑, @Mapping은 타입을 매핑.

  여기서 keyword는 analyzer 적용이 안 됨.

 

<elastic-settings.json>

{
  "analysis": {
    "analyzer": {
      "my_analyzer": {
        "type": "nori"
      }
    }
  }
}

 

<product-mappings.json>

 
{
  "properties": {
    "id": {
      "type": "keyword"
    },
    "productName": {
      "type": "text",
      "analyzer": "my_analyzer"
    },
    "price": {
      "type": "long"
    },
    "ranking": {
      "type": "Integer"
    }
  }
}

setting과 mapping을 해줬다.

 

ProductSearchRepository.java

ElasticsearchRepository를 상속받아 구현.

@Repository
public interface ProductSearchRepository extends ElasticsearchRepository<ProductDocument, Long> {

    List<ProductDocument> findByProductName(String productName, Pageable pageable);
}

 

ProductService.java

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;
    private final ProductSearchRepository productSearchRepository;

    public void saveProduct(SaveProduct saveProduct) {
        Product productEntity = productRepository.save(saveProduct.toEntity());

        productSearchRepository.save(
            ProductDocument.from(productEntity));
    }

    public List<SearchProduct.Response> findByProductName(
        SearchProduct.Request request, Pageable pageable) {
        return productSearchRepository.findByProductName(
                request.getProductName(), pageable)
            .stream()
            .map(Response::from)
            .collect(Collectors.toList());
    }
}

ProductController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/product")
@Slf4j
public class ProductController {
    private final ProductService productService;
    @PostMapping("/save")
    public ResponseEntity<Void> save(@RequestBody SaveProduct saveProduct) {
        productService.saveProduct(saveProduct);
        return ResponseEntity.ok().build();
    }

    @PostMapping("/search")
    public ResponseEntity<List<Response>> search(
        @RequestBody SearchProduct.Request request,
        Pageable pageable) {

        return ResponseEntity.ok(
            productService.findByProductName(request, pageable));
    }
}

 

테스트

키바나로 접속해서 Discover -> create Index pattern 해서 product* 이름의 인덱스 패턴을 생성한다. 

하는 도중에 product가 index로 등록되어있는 것이 보이는데 코드상에 @Document(indexName = "product") 했던 것이 생성된 것이다. 

 

인덱스 패턴을 생성하고 discover 에 들어가면 입력한 데이터가 잘 저장되어있는 것을 확인할 수 있다.

 

Dev tools 가서도 인덱스 잘 생성되었나 확인해볼 수 있다.


 

막상 구현하니까 공부했던 것만큼 어렵지가 않다.

이제껏 했던 거는 GCP에서 하느라 엄청 오래 걸렸던 것 같다.

상세한 검색옵션은 계속 추가해나가는 걸로 하고 이쯤에서 1차 구현 완료 !

 

참조

 

https://velog.io/@dktlsk6/Spring-boot-ElasticSearch-%EC%97%B0%EB%8F%99-%ED%95%98%EC%97%AC-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B2%80%EC%83%89-%EC%88%9C%EC%9C%84-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

https://velog.io/@backtony/Spring-Data-Elasticsearch-%EC%97%B0%EB%8F%99-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0