스프링 서버로 테스트 해보기
ES 의 검색 기능을 활용해보기 위해 자동완성과 일반적인 검색 기능에 대해서 구현해보기
- 목표
- 검색시 자동완성 기능 (
multi field,search_as_you_type,multi_match,bool_prefix) - 검색 기능 향상
- 오타 허용 (
fuzziness) - 조회시 평점 4.0 상위 노출 (
should) - 일치하는 키워드 하이라이팅 (
highlight) - 페이지네이션 (
from,size) - 동의어 설정 (
synonyms filter) - 한글, 영어 혼용 검색 기능 (
nori_tokenizer) - 상품명, 설명, 카테고리 가중치 검색 (3:1:2) (
multi_match) - 가격, 카테고리 필터링 (
multi field,range,term)
- 오타 허용 (
- 검색시 자동완성 기능 (
매핑과 세팅 정보
@Document(indexName = "products")
@Setting(settingPath = "/elasticsearch/product-settings.json")
public class ProductDocument {
@Id
private String id;
@MultiField(
mainField = @Field(type = FieldType.Text, analyzer = "products_name_analyzer"),
otherFields = {
@InnerField(suffix = "auto_complete", type = FieldType.Search_As_You_Type, analyzer = "nori")
}
)
private String name;
@Field(type = FieldType.Text, analyzer = "products_description_analyzer")
private String description;
@Field(type = FieldType.Integer)
private Integer price;
@Field(type = FieldType.Double)
private Double rating;
@MultiField(
mainField = @Field(type = FieldType.Text, analyzer = "products_category_analyzer"),
otherFields = {
@InnerField(suffix = "raw", type = FieldType.Keyword)
}
)
private String category;
}name,description,category는 multi match 로 처리해야 하므로 좀더 유연한 검색을 위해text타입으로 선언되었다name은 이름 자동완성을 위해 멀티 필드 및search_as_you_type타입으로 정의하였다- 그리고 각각의 analyzer 는 커스텀 analyzer 를 통해 처리
- 동의어
- nori (한글 토크나이저)
category또한 완전 일치하게 필터링해야하는 목표가 있으므로keyword용 필드를 따로 선언하였다
{
"analysis": {
"filter": {
"product_synonyms": {
"type": "synonym",
"synonyms": [
"samsung, 삼성",
"apple, 애플",
"노트북, 랩탑, 컴퓨터, computer, laptop, notebook",
"전화기, 휴대폰, 핸드폰, 스마트폰, 휴대전화, phone, smartphone, mobile phone, cell phone",
"아이폰, iphone",
"맥북, 맥, macbook, mac"
]
}
},
"analyzer": {
"products_name_analyzer": {
"char_filter": [],
"tokenizer": "nori_tokenizer",
"filter": ["nori_part_of_speech", "nori_readingform", "lowercase", "product_synonyms"]
},
"products_description_analyzer": {
"char_filter": ["html_strip"],
"tokenizer": "nori_tokenizer",
"filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
},
"products_category_analyzer": {
"char_filter": [],
"tokenizer": "nori_tokenizer",
"filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
}
}
}
}- 동의어 설정
- nori_tokenizer 를 통해 한글, 영어 혼용 토큰화 처리
자동완성 기능
public List<String> getSuggestions(String query) {
Query multiMatchQuery = MultiMatchQuery.of(m -> m
.query(query)
.type(TextQueryType.BoolPrefix)
.fields(
"name.auto_complete",
"name.auto_complete._2gram",
"name.auto_complete._3gram"
)
)._toQuery();
NativeQuery nativeQuery = NativeQuery.builder()
.withQuery(multiMatchQuery)
.withPageable(PageRequest.of(0, 5))
.build();
SearchHits<ProductDocument> searchHits =
elasticsearchOperations.search(nativeQuery, ProductDocument.class);
return searchHits.getSearchHits().stream()
.map(hit -> {
ProductDocument productDocument = hit.getContent();
return productDocument.getName();
})
.toList();먼저 BoolPrefix 를 통해 앞의 두 글자는 아무 위치나 포함되어도 조회되도록 하고 마지막 글자는 무조건 맨 앞의 글자와 일치해야 한다
- 간단히 이야기하면 자동완성을 정의하는 알고리즘이다
이후
name.auto_complete라는 멀티 필드에 대해2gram,3gram필드까지 조회하도록 한다2gram: 두 글자씩 묶어서 토큰화3gram: 세 글자씩 묶어서 토큰화
그 이후 5개까지 제한
GET /products/_search { "query": { "multi_match": { "query": "스마", "type": "bool_prefix", "fields": [ "name.auto_complete", "name.auto_complete._2gram", "name.auto_complete._3gram" ] } }, "size": 5 }위 코드의 실제 쿼리는 아래와 같다
실제 더미데이터로 테스트
- "곱창 돌김생김"
- "구운 돌김"
- "완도 곱창 돌김 100매"
"돌" 키워드로 검색

"곱" 키워드로 검색

"구운" 키워드로 검색

검색 기능
public List<ProductDocument> searchProducts(
String query,
String category,
double minPrice,
double maxPrice,
int page,
int size
) {
Query multiMatchQuery = MultiMatchQuery.of(m -> m
.query(query)
.fields(
"name^3",
"description^1",
"category^2"
)
.fuzziness("AUTO")
)._toQuery();
Query categoryFilter = TermQuery.of(t -> t
.field("category.raw")
.value(category)
)._toQuery();
Query priceRangeFilter = NumberRangeQuery.of(r -> r
.field("price")
.gte(minPrice)
.lte(maxPrice)
)._toRangeQuery()._toQuery();
Query ratingShould = NumberRangeQuery.of(r -> r
.field("rating")
.gt(4.0)
)._toRangeQuery()._toQuery();
Query boolQuery = BoolQuery.of(b -> b
.must(multiMatchQuery)
.filter(categoryFilter, priceRangeFilter)
.should(ratingShould)
)._toQuery();
HighlightParameters highlightParameters = HighlightParameters.builder()
.withPreTags("<b>")
.withPostTags("</b>")
.build();
Highlight highlight = new Highlight(highlightParameters, List.of(new HighlightField("name")));
HighlightQuery highlightQuery = new HighlightQuery(highlight, ProductDocument.class);
NativeQuery nativeQuery = NativeQuery.builder()
.withQuery(boolQuery)
.withHighlightQuery(highlightQuery)
.withPageable(PageRequest.of(page - 1, size))
.build();
SearchHits<ProductDocument> searchHits =
elasticsearchOperations.search(nativeQuery, ProductDocument.class);
return searchHits.getSearchHits().stream()
.map(hit -> {
ProductDocument productDocument = hit.getContent();
String highlightName = hit.getHighlightField("name").get(0);
productDocument.setName(highlightName);
return productDocument;
})
.toList();
}- 이름, 설명, 카테고리 세 가지 필드에 대한 키워드 검색을 위해 멀티 매치 및 가중치 적용 → 3:1:2
- 카테고리 키워드 필드에 대한 완전한 매치를 위해
term적용 - 가격에 대한 Range Filter
- rating 에 대한 Range Filter
- 각각의 쿼리를 종합 → BoolQuery (must, filter, should)
- 하이라이트 적용
```
GET /products/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "삼성",
"fields": ["name^3", "description", "category^2"],
"fuzziness": "AUTO"
}
}
],
"filter": [
{
"range": {
"price": {
"gte": 10000,
"lte": 50000
}
}
},
{
"term": {
"category.raw": "가전제품"
}
}
],
"should": [
{
"range": {
"rating": {
"gt": 4.0
}
}
}
]
}
},
"highlight": {
"fields": {
"name": {
"pre_tags": ["<b>"],
"post_tags": ["</b>"]
}
}
},
"from": 0,
"size": 5
}
```- 위 코드를 실제 쿼리로 적용한 결과
오타인 경우 검색
- 삼성 스마트 → 삼성 스파티

영어로도 검색
- 삼성 → samsung

테스트한 코드
'학습일지 > ElasticSearch' 카테고리의 다른 글
| [ElasticSearch] ES 맛보기 - 역인덱스(Inverted Index) (1) | 2025.09.27 |
|---|