상세 컨텐츠

본문 제목

[Elastic search] 데이터 검색

Elastic Search

by gyu.ree 2023. 4. 29. 09:04

본문

엘라스틱 서치는 인덱스에 저장된 문서를 검색할 수 있도록 다양한 검색 기능을 제공한다.

문서는 색인시 설정한 분석기에 의해 분석 과정을 거쳐 토큰으로 분리되는데, 이러한 분석기는 색인 시점에 사용할 수 있지만 검색 시점에 사용하는 것도 가능하다.

활용 가능한 검색 기능들을 알아보고, 스럽에서 어떻게 적용하고 있는지 살펴보자 ! 

 

(분석기에 대한 내용 아래 링크 참고)

 

[Elastic Search] 데이터 모델링

엘라스틱서치에서는 색인할 때 문서의 데이터 유형에 따라 필드에 적절한 데이터 타입을 지정해야한다. 이러한 과정을 매핑이라하며, 매핑은 색인된 문서의 데이터 모델링이라고도 할 수 있다.

gyu-ree.tistory.com

 

특정 문장이 검색어로 요청되면 분석기를 통해 분석된 토큰의 일치 여부를 판단해서 그 결과에 점수를 매긴다. 그리고 이를 기반으로 순서를 적용해 결과를 사용자에게 최종적으로 출력하게 된다.

 

1. 검색 API 

엘라스틱 서치는 색인 시점에 Analyzer를 통해 분석된 텀을 Term, 출현빈도, 문서번호와 같이 역색인 구조로 만들어 내부적으로 저장한다. 검색 시점에는 Keyword 타입과 같은 분석이 불가능한 데이터와 Text 타입과 같은 분석이 가능한 데이터를 구분해서 분석이 가능한 경우 분석기를 이용해 분석을 수행한다. 

이를 통해서 검색 시점에도 텀을 얻을 수 있으며, 해당 텀으로 역색인 구조를 이용해 문서를 찾고 이를 통해 스코어를 계산해서 결과로 제공한다. 

 

엘라스틱서치에서는 기본적으로 쿼리 기반으로 동작하는데, URI로 표기하는 방법과 RESTful API를 이용하는 방법이 있다. 

 

- URI 방식

ex.

 

GET movie_search/_search?q=prdtYear:2018

 

GET 메소드에 쿼리 스트링을 이용해서 데이터를 전달한다. 

 

 

- Restful API 방식 (Request Body)

ex.

 

POST /movie_search/_search
{
  "query":{
    "term":{"prdtYear":"2018"}
  }
}

 

 

보기에는 uri 방식이 편해보이지만, 엘라스틱서치가 제공하는 검색 API를 모두 활용하기 위해서는 반드시 Request Body 방식을 사용해야한다. 

(URI 검색은 검색 조건을 몇 가지만 추가해도 검색식이 복잡해져서 사용하기가 불편하다.) 

 

엘라스틱 서치에서는 정밀한 검색을 위해 JSON 구조를 기반으로 한 Query DSL을 제공한다. Query DSL은 엘라스틱 서치의 가장 강력한 기능 중 하나로서 Request Body 검색을 이용할 때 사용하는 JSON 구조를 지원한다. 

 

 

2. Query DSL 

Query DSL로 쿼리를 작성하려면 미리 정의된 문법에 따라 JSON 구조로 작성해야한다. 아래와 같은 구조를 가진다. 

 

{

"size" : [리턴받는 결과의 개수 지정]

"from" : [몇 번째 문서부터 가져올지 지정]

"timeout" : [검색을 요청해서 결과를 받는 데까지 걸리는 시간]

"_source" : {} [검색 시 필요한 필드만 출력하고 싶을 때]

"query" : {} [검색 조건문]

"aggs" : {} [통계 및 집계 데이터 사용할 때 사용]

"sort" : {} [문서 결과를 어떻게 출력할지에 대한 조건을 사용]

}

 

 

Query DSL을 이용해 검색 질의를 작성할 때 조금만 조건이 복잡해지더라도 여러 개의  작은 질의를 조합해서 사용해야한다.

이 질의는 두가지 형태로 나눠서 생각해볼 수 있다.

 

1) 실제 분석기에 의한 전문 분석이 필요한 경우 (= 쿼리 컨텍스트)

     전문 검색 시 필요한 경우 사용

 

- 문서가 쿼리와 얼마나 유사한지를 스코어로 계산한다.

- 질의가 요청될 때마다 엘라스틱서치에서 내부의 루씬을 이용해 계산을 수행한다.

- 일반적으로 전문 검색에 많이 사용한다.

- 캐싱되지 않고 디스크 연산을 수행하기 때문에 상대적으로 느리다.

 

스럽에서는 기능 특성 상 모두 쿼리 컨텍스트 방법을 사용하고있다. 

 

 

 

2) 단순히 YES/NO로 판단할 수 있는 조건 검색 (=필터 컨텍스트)

     Yes/No로 판단할 수 있는 경우 사용

 

- 쿼리의 조건과 문서가 일치하는지를 구분한다.

- 별도의 스코어를 계산하지 않고 단순 매칭 여부를 검사한다.

- 자주 사용되는 필터의 결과는 엘라스틱서치가 내부적으로 캐싱한다.

- 기본적으로 메모리 연산을 수행하기 때문에 상대적으로 빠르다.

 

 

3.  Query DSL 의  주요 파라미터

Query DSL은 다양한 파라미터를 옵션으로 제공한다. 위 글에서는 스럽에서 사용하고 있는 파라미터만 소개할 예정이다 ! 

 

페이징 

페이징을 구현하기 위해서 두가지 파라미터를 사용한다.

 

from: 문서의 시작 나타낸다.

size: 문서의 개수를 나타낸다. 

 

 

# 첫번째 페이지 요청
POST movie_search/_search
{
	"from" : 0,
    "size" : 5,
    "query" : {
    	"term" : {
        "repNationNm" : "한국"
        }
    }
}

# 두번째 페이지 요청
POST movie_search/_search
{
	"from" : 5,
    "size" : 5,
    "qeury" : {
    	"term" : {
        "repNationNm" : "한국"
        }
    }
}

 

 

주의할점은 설정된 페이지를 제공하기 위해서는 전체를 읽어서 사이즈만큼 필터링해서 제공하는 구조라는 것이다.

 

적용하기

 

 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).trackTotalHits(true).size(1000);

 

스럽에서는 BoolQueryBuilder와 SearchSourceBuilder를 이용해 QueryDSL을 사용하고 있다. 뒤에 다시 설명할테니 여기서는 size를 1000으로 지정했다는 것만 확인하자. 

 

 

 

쿼리 결과 정렬

엘라스틱서치가 기본적으로 계산한 유사도에 의한 스코어 값으로 정렬하는 것이 아니라 필드의 이름이나 가격, 날짜 등을 기준으로 재정렬하고싶은 경우 사용한다.

 

asc와 desc도 제공해주는데 아래 예시에서는 오름차순으로 정렬하도록 한다. 

 

POST movie_search/_search
{
	"query" : {
    	"term" : {
        	"repNationNm" : "한국"
        }
    },
    "sort" : {
    	"prdtYear" : {
        	"order" : "asc"
        	}
        }
}

 

 

검색결과 필드 지정

검색 요청 시 _source를 지정하여 원하는 데이터만 출력할 수 있다. 아래의 경우 결과로 movieNm 필드만 필터링되어 출력된다. 

 

POST movie_search/_search
{
	"_source" : [
    	"movieNm"
    ],
    "query" : {
    	"term" : {
        	"repNationNm" : "한국"
            }
    }
}

 

 

범위 검색

숫자나 날짜 데이터의 경우 지정한 값이 아닌 범위를 기준으로 질의해야하는 경우 사용한다. 

 

문법 연산자 설명
lt < 피연산자보다 작음
gt > 피연산자보다 큼
lte <= 피연산자보다 작거나 같다
gte >= 피연산자보다 크거나 작다

 

POST movie_search/_search
{
	"query":{
    	"range" : {
			"prdtYear" : {
            	"gte" : "2016",
                "lte" : "2017"
            }	        
        }
    }

}

 

 

operator 설정

OR 연산이나 AND 연산의 사용

엘라스틱 서치는 검색 시 문장이 들어올 경우 기본적으로 OR 연산으로 동작한다. 하지만 operator 파라미터에 and 연산자를 명시해주면 and로 동작한다.

 

POST movie_search/_search
{
	"query" : {
    	"match" : {
        	"movieNm" : {
            	"query" : "elastic search" ,
                "operator" : "and"
            }
        }
    }
}

 

operator 파라미터를 생략하면 or 연산으로 동작해서 "elastic" 단어 혹은 "search" 가 모두 들어있는 문서가 검색되지만 and로 명시했기 때문에 두 단어 모두 들어간 경우만 검색된다. 

 

 

minimum_should_match 설정

or 연산을 수행할 경우 검색 결과가 너무 많아질 수 있다. 이 경우 텀의 개수가 몇 개 이상 매칭될 때만 검색 결과로 나올 수 있게 할 수 있는데 이때 사용하는 파라미터가 minimum_should_match 이다. 

 

POST movie_search/_search
{
	"query" : {
    	"match" : {
        	"movieNm" : {
            	"query" : "elastic search test" ,
                "minimum_should_match" : 2
            }
        }
    }
}

 

적용하기

 

BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                    .should(query1).should(query2).should(query3).should(query4).should(query5).minimumShouldMatch(1);

 

minimumSouldMatch를 1로 설정하고 있다.

 

 

boost 설정

관련성이 높은 필드나 키워드에 가중치를 더 줄수 있게 한다. 

 

POST movie_search/_search
{
	"query" : {
    	"multi_match" : {
        	"query" : "Fly",
            "fields" : ["movieNm^3", "movieNmEn"]
        }
    }
}

 

Fly 라는 단어를 movieNm과 movieNmEn 이라는 두 개의 필드에서 조회하는데, 만약 한글 영화 제목이 일치하게 된다면 한글 영화 제목에서 계산되는 스코어에 가중치 값으로 3을 곱하게 된다.

 

적용하기 

 

final String searchTerm = dto.getSearchTerm();
            final QueryBuilder query1 = QueryBuilders.matchQuery("nickname", searchTerm).boost(3);
            final QueryBuilder query2 = QueryBuilders.matchPhraseQuery("nickname_ngram", searchTerm);
            final QueryBuilder query3 = QueryBuilders.matchPhraseQuery("nickname_kr_eng2kor", searchTerm);
            final QueryBuilder query4 = QueryBuilders.matchPhraseQuery("nickname_kr_chosung", searchTerm);
            final QueryBuilder query5 = QueryBuilders.termQuery("nickname_kr_jamo", searchTerm);

 

 

nickname 필드에 가중치 3을 더 주고있다.

 

3.  Query DSL 의 주요 쿼리

Match All Query 

색인에 모든 문서를 검색하는 쿼리다. 가장 단순한 쿼리로서 일반적으로 색인에 저장된 문서를 확인할 때 사용된다. 

 

POST movie_search/_search
{
	"query" : {
    	"match_all" : {}
    }
}

 

 

Match Query 

match query는 텍스트, 숫자, 날짜 등이 포함된 문장을 형태소 분석을 통해 텀으로 분리한 후 이 텀들을 이용해 검색 질의를 수행한다.

POST movie_search/_search
{
	"query" : {
    	"match" : {
        	"movieNm" : "그대 장미"
        }
    }
}

"그대 장미"라는 검색어를 Match Query로 요청했기 때문에 엘라스틱 서치는 해당 질의를 받으면 검색어에 대해 형태소 분석을 통해 " 그대" "장미" 라는 2개 텀으로 분리한다.operator  필드가 지정돼있지 않기 때문에 OR 연산으로 검색한다.

 

적용하기

 

final QueryBuilder query1 = QueryBuilders.matchQuery("nickname", searchTerm).boost(3);

 

 

Multi Match Query

match Query와 기본적인 사용방법은 동일하나 단일 필드가 아닌 여러개의 필드를 대상으로 검색해야 할 때 사용한다.

 

POST movie_search/_search
{
"query": {
	"multi_match" : {
    	"query" : "가족",
        "fields" : ["movieNm","movieEm"]
        }
}
}

 

movieNm 혹은 movieNmEn 필드에서 '가족'이 포함된 모든 문서를 검색한다. 

 

 

Term Query

엘라스틱서치는 텍스트 형태에서는 두가지 매핑 유형을 지원한다.

 

Text: 필드에 데이터가 저장되기 전에 데이터가 분석되어 역색인 구조로 저장된다.

keyword: 데이터가 분석되지 않고 그대로 필드에 저장된다.

 

Term query는 별도의 분석 작업을 수행하지 않고 입력된 텍스트가 존재하는 문서를 찾는다. 따라서 keyword 데이터 타입을 사용하는 필드를 검색한다. 텀 쿼리는 필드에 텀이 정확히 존재하지 않는 경우 검색이 되지 않기 때문에 주의하자!

 

final QueryBuilder query5 = QueryBuilders.termQuery("nickname_kr_jamo", searchTerm);

 

 

Bool Query

엘라스틱서치에서는 하나의 쿼리나 여러 개의 쿼리를 조합해서 더 높은 스코어를 가진 쿼리 조건으로 검색을 수행할 수 있다. 이러한 유형의 쿼리를 Compound Query라고 한다. (스럽에서는 Compound Query를 사용하고있다.)

이러한 Coumpound Query를 구현하기 위해 엘라스틱 서치에서는 Bool Query를 제공한다.  Bool Query를 상위에 두고 하위에 다른 Query들을 사용해 복잡한 조건의 쿼리문을 작성할 수 있다.

 

문법적으로 제공되는 속성은 4가지가 있다. 

 

Elasticsearch SQL 설명
must AND 칼럼 = 조건 반드시 조건에 만족하는 문서만 검색된다
must_not AND 칼럼 != 조건 조건을 만족하지 않는 문서가 검색된다
should or 칼럼 = 조건 여러 조건 중 하나 이상을 만족하는 문서가 검색된다
filter 칼럼 IN (조건) 조건을 포함하고 있는 문서를 출력한다

 

적용하기

 

final String searchTerm = dto.getSearchTerm();
            final QueryBuilder query1 = QueryBuilders.matchQuery("nickname", searchTerm).boost(3);
            final QueryBuilder query2 = QueryBuilders.matchPhraseQuery("nickname_ngram", searchTerm);
            final QueryBuilder query3 = QueryBuilders.matchPhraseQuery("nickname_kr_eng2kor", searchTerm);
            final QueryBuilder query4 = QueryBuilders.matchPhraseQuery("nickname_kr_chosung", searchTerm);
            final QueryBuilder query5 = QueryBuilders.termQuery("nickname_kr_jamo", searchTerm);

            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                    .should(query1).should(query2).should(query3).should(query4).should(query5).minimumShouldMatch(1);

 

 

지금까지 elastic seaerch에서 제공해주는 유용한 파라미터와 쿼리를 살펴봤다.

기능에 따라서 적절한 도구들을 잘 활용하여 최적의 결과를 뽑을 수 있도록 하자 ! 

 

관련글 더보기

댓글 영역