////
Search
Duplicate
9️⃣

9장. 검색 엔진으로 활용하기

inverted index의 의미와 검색에서의 역할
analyzer의 역할과 설정 방법
search API를 통해 검색 쿼리를 날리는 방법
ElasticSearch에서 제공하는 다양한 query DSL의 종류와 사용방법

9.1 inverted index란

‘I am a boy’ → I, am, a, boy
‘You are a girl’ → You, are, a, girl
: 위와 같이 나뉜 단어들을 토큰이라고 부르며 토큰을 만들어 내는 과정을 토크나이징이라 함, 나눠진 토큰들은 다음과 같은 형태로 저장되는데 이를 inverted index라고 한다.
Tokens
Documents
I
1
am
1
a
1,2
boy
1
girl
2
You
2
are
2
: 이렇게 나뉜 토큰들을 바탕으로 inverted index를 검색
: 만약 대문자 I가 아닌 소문자 i를 검색했다면 어떻게 될까, 대문자 I는 있으나 소문자 i는 존재하지 않으므로 아무것도 검색되지 않는다.
: 결과를 얻으려면 토큰이 정확하게 일치해야 한다.
analyze API
: 문자열이 어떻게 토크나이징되는지 확인할 수 있음
: 토크나이징에 standard analyzer를 사용한다는 의미, analyzer를 변경할 때마다 토크나이징 결과가 달라짐
코드

9.2 analyzer

: inverted index는 저장된 문서들의 필드 값을 어떻게 분석해 놓았는지 저장하는 중요한 데이터
: 해당 데이터를 만드는 데 사용되는 것이 analyzer
ElasticSearch의 analyzer 구성
character filter
: 1차로 처리를 담당
: <, >, ! 등과 같은 의미없는 특수문자들을 제거하거나 HTML 태그를 제거
: 문자열을 구성하고 있는 문자들을 특정한 기준으로 변경
tokenizer
: 일정한 기준(공백이나 쉼표 등에 의해 문자열을 n개의 토큰으로 나눔
token filter
: tokenizer까지 거쳐서 n개의 토큰이 생성되면 이 토큰들을 token filter가 다시 한번 변형, 토큰을 전부 소문자로 바꾸는 lwercase token filter가 해당
: analyzer를 구성할 때는 tokenizer를 필수로 명시해야 하며 하나의 tokenizer만 설정할 수 있다. 반면 character filter와 token filter는 필요하지 않을 경우 생략
standard analyzer
: 한 개의 tokenizer와 3개의 token filter로 구성
: standard tokenizer는 unicode standard annex라는 룰에 따라 문자열을 분리
: lowercase token filter에 의해 전부 소문자로 변형

9.3 analyzer와 검색의 관계

: analyzer를 통해 생성한 토큰들이 inverted index에 저장되고 검색할 때는 해당 inverted index에 저장된 값을 바탕으로 문서를 찾음
: 검색 니즈를 잘 파악하여 적합한 analyzer를 설정해야 하며, 기존 인덱스에 설정한 analyzer를 바꾸고 싶을 때는 인덱스를 새로 만들어 재색인해야 함
타입에 따라 달라지는 analyzer
: text 타입의 기본 analzyer는 standard analzyer이고 keyword 타입의 기본 analzyer는 keyword analzyer이다.
: 각각의 analzyer가 각각의 필드에 저장한 내용은 다음과 같다.
standard analzyer
: ElasticSearch Training Book → elasticsearch, training, book
keyword analyzer
: ElasticSearch is cool open source search engine → ElasticSearch is cool open source search engine
: 이처럼 analyzer에 따라서 검색 결과와 품질이 달라지기 때문에 사용자의 검색 니즈를 파악해서 그에 맞는 analyzer를 사용하는 것이 중요

9.4 Search API

: search API는 URI Search 형태도 제공하고 RequestBody 작성을 통해서 다양한 옵션을 추가한 RequestBody Search 형태도 제공한다.
: index_name의 경우에는 한 개 이상의 인덱스를 지정해서 다수의 인덱스에 동시 쿼리를 날릴 수도 있다.
: curl -X GET “localhost:9200/_all/_search?q=user:kimchy”
Request Body
옵션
내용
query
실제 검색을 위한 쿼리문을 지정
from/size
검색 결과를 n개의 단위로 나눠서 볼 때 사용한다.
sort
검색 결과를 _score가 아닌 별도의 필드를 기준으로 정렬한다.
source
검색 결과 중 특정 필드의 내용만을 보고자 할 때 사용한다.
highlighting
검색 결과 중 검색어와 매칭하는 부분을 강조하기 위해서 사용한다.
boost
검색 결과로 나온 스코어를 변경할 때 사용한다.
scroll
검색 결과를 n개의 단위로 나눠서 볼 때 사용한다. from/size와 유사하지만 scroll id를 통해서 다음번 검색 결과를 가져올 수 있다.
from/size
: 검색 결과의 개수가 일정 수준 이상일 때 검색 결과를 가져올 시작점부터 끝점까지를 설정하는 용도로 사용
: database의 offset, limit 개념으로 보면 될 듯
sort
: 검색 결과를 특정 필드 기준으로 정렬할 때 사용
: 다른 필드를 기준으로 검색한 결과가 필요할 경우 사용
: keyword나 integer와 같이 not analyzed가 기본인 필드를 기준으로 해야 함
source
: 검색 결과 중 특정한 필드의 값만 찾아볼 때 사용
: 불필요한 필드는 가져오지 않고 필요한 필드만 가져오게할 때 사용
highlighting
: 어떤 부분이 검색어와 일치해서 검색 결과에 포함되었는지 확인할 수 있음
boost
: 검색 결과로 나온 스코어를 변경할 때 사용
: 특정 검색 쿼리의 스코어를 높이거나 낮추고 싶을 때 사용
scroll
: from/size와 유사해보이는 기능이지만 검색 당시의 스냅샷을 제공해 준다는 점이 다름
: from/size의 경우 pagnation을 하는 동안에 새로운 문서가 인입되면 기존 검색 결과에 영향을 주지만 scroll 옵션을 사용하면 새로운 문서가 인입되도 변하지 않음
: scroll_id가 뭔지는 안나와있다.. 조금더 알아보자
: scroll_id를 유지하는 기간을 설정해야하는데, 이때 힙 메모리 사용량에 영향을 주므로 필요한 만큼만 설정

9.5 Query DSL이란

: 검색 쿼리는 Query DSL이라 불리며 크게 Query Context와 Filter Context로 분류
옵션
내용
Query Context
Full text search를 의미하며 검색어와 문서가 얼마나 매칭되는지를 표현하는 score를 가짐
Filter Context
검색어가 문서에 존재하는지 여부를 Yes나 No 형태의 검색 결과로 보여준다. Query Context와는 다르게 score 값을 가지지 않음

9.6 Query Context

Query Context 종류
종류
내용
match
검색어가 토크나이징된 토큰들이 존재하는지 여부를 확인
match_phrase
match와 비슷하지만 검색어에 입력된 순서를 지킴
multi_match
match와 동작 원리는 같으며 다수의 필드에 검색하기 위해 사용
query_string
and와 or 같이 검색어 간 연산이 필요할 때 사용
9.6.1 match 쿼리
: match 쿼리는 Query Context 중에서도 가장 많이 사용되는 쿼리로 검색어로 들어온 문자열을 analyzer로 분석한 후, inverted index에서 해당 문자열의 토큰을 가지고
있는 문서를 검색한다.
: 문서의 해당 필드에 설정해 놓은 analyzer를 기본으로 사용하며 별도의 analyzer를 사용할 때는 직접 명시해주면 된다.
curl -X POST "localhost:9200/test_data/_search?pretty" -H 'Content-Type: application/json' -d' { "query": { "match": { "description": "nignx guide" } } }
YAML
복사
: nginx와 guide라는 두 개의 토큰으로 만들고 inverted index를 조회해서 두 개의 토큰이 가장 많이 포함된 문서들을 기준으로 _score를 생성해서 결과를 반환
: match 쿼리는 analyzer를 통해서 분석한 토큰을 기준으로 검색하며 이때 어떤 토큰이 먼저 있는지에 대한 순서는 고려하지 않는다.
: 즉 검색어에 nginx guide가 들어오든 guide nginx가 들어오든 같은 결과를 반환한다.
9.6.2 match_phrase 쿼리
: match가 analyzer를 통해 생성된 토큰들의 순서를 고려하지 않는 것과 달리 match_phrase는 검색어의 순서도 고려한다.
: Kernel Linux과 같은 단어를 검색했을때, match 쿼리의 경우 이 검색어를 kernel과 linux라는 두 개의 토큰으로 만들고 두 개의 단어 중 하나라도 포함되어 있다면
: 검색 결과를 보여주지만 match_phrase는 kernel linux라는 두 개의 토큰을 만드는 것은 동일하지만 kernel linux라는 정확한 순서를 가진 단어를 찾는다.
: 즉 두 개의 단어 중 하나만 포함되어 있다면 검색 결과를 보여주지 못한다.
9.6.3 mutli_match 쿼리
: 동작방식은 match 쿼리와 동일하나 두 개 이상의 필드에 날릴 수 있다는 점에서 다르다.
9.6.4 query_string 쿼리
: query_string은 and와 or 같은 검색어 간 연산이 필요한 경우에 사용, 경우에 따라 match 쿼리나 mutli_match 쿼리와 동일하게 동작할 수 있음
: 외에도 regular expression 기반의 쿼리가 될 수도 있다.
match
{ "query": { "match": { "title" : "Linux" } } }
YAML
복사
query_string
{ "query": { "query_string": { "fields": ["title"], "query": "Linux" } } }
YAML
복사
와일드카드
{ "query": { "query_string": { "fields": ["title"], "query": "*nux" } } }
YAML
복사
: query_string을 통한 와일드카드 검색은 스코어링을 하지 않을 뿐더러 검색 성능에 좋지 않기 때문에 사용하지 않는 것이 좋다.

9.7 Filter Context

: Filter Context는 Term Level Query라고도 부르며 이름에서 알 수 있듯이, 해당 문서에 대한 필터링에 사용되는 쿼리
: Query Context가 검색어가 문서에 얼마나 매칭되는지를 계산하고 찾는다면 Filter Context는 검색어의 포함 여부를 찾는 형태, 차이는 검색어를 analyze하느냐의 여부
종류
내용
term
검색어로 입력한 단어와 정확하게 일치하는 단어가 있는지를 찾는다.
terms
term과 유사하지만 여러 개의 단어를 기준으로 하나 이상 일치하는 단어가 있는지 찾는다.
range
특정 범위 안에 있는 값이 있는지 찾는다.
wildcard
와일드카드 패턴에 해당하는 값이 있는지 찾는다.
9.7.1 term 쿼리
: term 쿼리는 정확하게 일치되는 단어를 찾을 때 사용, analyze를 하지 않기 때문에 당연히 대소문자를 구분함
: Linux를 검색할 경우, Linux에 대한 검색결과만 얻을 수 있음, 그러므로 text 타입의 필드에 검색할때는 주로 match를 사용
9.7.2 terms 쿼리
: terms 쿼리는 둘 이상의 term을 검색할 때 사용하는 쿼리이며 다수의 단어를 한 번에 검색할 때 사용
9.7.3 range 쿼리
: range 쿼리는 범위를 지정하여 특정 값의 범위 이내에 있는 경우를 검색할 때 사용
: release_date와 같은 날짜, 숫자 값들에 대해 사용
9.7.4 wildcard 쿼리
: 와일드카드 특수문자를 이용한 일종의 Full-Scan 검색이 가능한 쿼리, text 필드가 아닌 keyword 타입의 쿼리에 사용해야 함
: 모든 inverted index를 하나하나 확인하기 때문에 검색 속도가 매우 느리다. 시작부터 *를 포함하는 쿼리는 문서의 개수가 늘어날수록 검색 결과도 선형적으로 늘어나므로 주의
: 사실 wildcard 쿼리를 사용할 때는 꼭 필요한가에 대한 재고가 필요하다.단순히 match 쿼리를 사용하는 편이 더 낫다.

9.8 bool query를 이용해 쿼리 조합하기

항목
설명
스코어링
캐싱
must
항목 내 쿼리에 일치하는 문서를 검색
O
X
filter
항목 내 쿼리에 일치하는 문서를 검색
X
O
should
항목 내 쿼리에 일치하는 문서를 검색
O
X
must_not
항목 내 쿼리에 일치하지 않는 문서를 검색
X
O
: 위와 같은 bool query의 must, should는 Query Context에서 사용, filter, must_not은 Filter Context에서 사용
: bool query는 Query/Filter Context를 혼합해서 사용
{ "query": { "bool": { "must": [ { "match": { "title": "nginx" } } ], "filter": [ { "range" : { "release_date": { "gte" : "2016/01/01", "lte" : "2017/12/31" } } ] } } }
YAML
복사
: 위의 쿼리의 경우, 제목이 nginx이면서 발행날짜가 2016.01.01 ~ 2017.12.31에 해당하는 결과를 반환
: filter context에 포함되는 쿼리들은 filter에 넣는 것이 좋음, must 절에 포함시켜도 동일한 결과가 나오지만 실제로 실행해보면 더 느리다.
⇒ 이는 Filter Context들은 score를 계산하는 데 활용되기 때문에 불필요한 연산이 들어가지만 filter에 포함될 경우, score 계산이 발생하지 않기 때문

9.9 마치며

1.
ElasticSearch는 analyzer로 문서와 각 필드를 분석해서 토큰을 생성한다.
2.
analyzer를 통해서 생성된 토큰은 inverted index에 저장된다.
3.
analyzer를 변경하면 기존에 생성한 토큰들이 의미를 잃기 때문에 다시 인덱싱해야 한다.
4.
어떤 anlyzer를 사용했느냐에 따라서 생성되는 토큰이 다르기 때문에 같은 검색어에 대한 결과도 다를 수 있다. 의도한 대로 검색되지 않는다면 analyzer API를 통해서 해당
필드의 analyzer가 검색어를 포함한 토큰을 생성하는지 확인해야 한다.
5.
search API는 여러 가지 옵션을 제공한다. 이를 통해서 검색 결과를 정렬하거나 from/size 혹은 scroll을 이용해서 pagination을 할 수 있다.
6.
query 문은 크게 query context는 각 문서가 검색어와 얼마나 연관이 있는지 _score를 통해 순위를 매기고 filter context는 각 문서에 검색어가 포함되어있는지 여부만 계산
7.
filter context는 비트맵 형태로 결과가 캐싱되기 때문에 빠른 검색 결과를 보여준다.
8.
query context와 filter context는 bool query로 조합할 수 있으며 이를 통해서 다양한 형태의 검색을 진행할 수 있다.