개요
검색엔진에서 랭킹은 상당히 중요하다.
검색 결과가 10개 수준인 경우 랭킹이 잘못되어도 품질에서 차이가 크게 안 날 수 있지만, 검색 결과가 수 천, 수 만개인 경우 품질에 상당한 차이가 발생된다.
ES에서 랭킹을 어떻게 조정할 수 있는지, 랭킹에는 어떤 요소들이 있는지 등을 살펴본다.
우선 첫 번째로 ES에서 스크립트를 작성하는 방법부터 알아보자.
ES에서 스크립트 작성하기
- ES에서 script_score를 이용해서 커스터마이징 랭킹을 구현할 수 있다.
- script_score는 script 언어를 이용해서 코드를 작성하는 방식이다.
- script 언어는
painless
,expression
,mustache
,java
를 지원한다.
script language
💡 painless가 훨씬 편하다. painless로 구현하고, 성능 이슈가 있을 때 expression을 써도 늦지 않을 듯하다. 언어와 Search Templates은 같이 사용할 수 있다.
- painless
- 엘라스틱서치 자체에서 개발한 스크립트 언어.
- 기본으로 적용되며, Java Style의 문법으로 작성한다.
- 안전하고, 쉽고, 빠르다.
- 쿼리 실행, 데이터 전송, 텍스트 분석, 정렬 알고리즘 등의 다양한 목적으로 사용한다.
- expression
- 숫자 연산에 특화되어 있는 Lucene 기반 스크립트 언어.
- 숫자 연산에서는 매우 빠른 성능을 보장한다.
- 숫자 연산에만 특화되어 있기 때문에 I/O나 동적 메서드 호출, 루프 연산, 조건 연산 등은 불가능하다.
- 커스텀 랭킹이나 정렬 기능을 구현하는 것에 특화되어 사용한다.
- 다만 거리 연산 등을 직접 지원하지 않기 때문에.. 거리 개념이 들어가거나 함수가 포함되면 구현이 불가능해질 수 있다.
- mustache
- 스크립트 언어라기보다는 Search Templates이다.
- 미리 구성한 Query DSL에 특정 부분을
{{variable}}
과 같이 변수화해서 템플릿을 만들어두고, 변수만 대입하여 쿼리를 실행할 수 있다.
painless
- 기본 가이드
- 정말 많은 용도에 가져다 쓸 수 있다.
- 쿼리 실행, 정렬 알고리즘, 데이터 업데이트, 데이터 일부 삭제 등
- script cache
- 스크립트를 캐싱해두어 매번 스크립트를 가져오는 비효율을 제거하는 방법
- 캐시는 디폴트로 적용되어 업데이트가 발생했을 때만 컴파일된다. expire, max_size에 따라 캐시가 퇴출될 수 있다.
- 스크립트 언어를 사용하면, 매번 결과를 새로 연산해야하기 때문에 기본 동작에 비해 느릴 수 밖에 없다.
- 만약 매번 검색 시마다 달라지는 결과(유사도, 특정 좌표와의 거리 등)가 아니라면 ingest pipeline으로 결과를 미리 생성하여 문서에 포함시키는 것으로 속도를 향상시킬 수 있다.
- Dissecting(문자열 패턴 파싱), Grok(로그 파싱)도 가능하다.
빌트인 변수
변수명 | 설명 |
---|---|
ctx | context |
현재 문서(doc)의 정보를 담고 있음 | |
ctx._index | 인덱스명 |
ctx._id | 문서ID |
ctx._source | 문서의 소스 데이터 |
params | params에서 전달한 값을 get() 또는 [] 리스트 접근자로 사용할 수 있음 |
doc | doc의 각 필드에 접근할 수 있음 (e.g. doc['fieldName']) 열 기반으로 필드가 저장되어 있어서 접근하려는 필드 이외를 처리하지 않기에 성능이 뛰어나다. 값이 없는 경우 에러가 발생하기에 doc.containsKey('field')로 체크하는 것이 좋다. 분석된 텍스트 필드에는 접근이 안 된다. 접근을 가능하게 하려면 fielddata를 활성화해야 한다. |
_score | 해당 문서의 유사도 스코어 |
_source | _doc과 같이 소스 문서의 필드에 직접 접근을 할 수 있으나, 문서 전체를 파싱을 한 후 접근하는 방식이어서 일반적으로 _doc보다 성능이 떨어진다. 결과당 여러 필드를 반환하는데에 최적화되어 있다. 검색 결과로 히트된 N개의 문서에서 script fields를 생성할 때 사용하는 것이 좋다. map-of-maps 구조여서 _source.name.first와 같이 접근할 수 있다. |
사용 방법
- 전체 가이드
- 주석
// single-line comment
/* multi- line comment */
- Keywords (Built-in Functions)
- if, else, while, for, in, continue 등의 Java에서 사용할 수 있는 키워드들
- Functions
- 스크립트의 시작부에 선언할 수 있다.
boolean isNegative(def x) { x < 0 } ... if (isNegative(someVar)) { ... }
- Lambda
- Java의 Lambda와 동일하게 사용할 수 있다.
- 정규 표현식 (Regexes)
Pattern p = /[aeiou]/
Query DSL - Ranking
Rank feature query
- numeric 타입의 rank_feature, rank_features 필드를 기반으로 유사도 점수(relevance score)를 조정하는 방법
- 즉 pagerank, url_length, topics 등의 이미 랭크를 매기기 위한 점수들을 필드로 만들어뒀을 때, 유사도 점수에 가중치를 부여하여 조정하는 방법이다.
- 최근성이나 거리 계산과 같이 쿼리를 할 때마다 달라지는 경우에는 사용할 수 없다.
Script Query
- 쿼리를 할 때 script를 사용해서 필터링, 가공을 할 수 있는 방법
- Runtime fields와 유사하다
- params로 파라미터를 줄 수 있다.
GET /_search
{
"query": {
"bool": {
"filter": {
"script": {
"script": {
"source": "doc['num1'].value > params.param1",
"lang": "painless",
"params": {
"param1": 5
}
}
}
}
}
}
}
Script score
- default로 사용하는 유사도 스코어 이외에, 본인이 스코어를 정의하고 싶은 경우 사용한다.
- 쿼리 내에서 script_score를 사용하는 방법
- 유사도 스코어 펑션 자체가 비용이 커서 이미 필터링된 데이터에 대해 계산이 필요할 때 유용하다.
- 사실상 유사도 스코어를 대체하기 위한 방법이라고 봐야할 듯
- Script_score가 계산한 최종 점수는 반드시 0 또는 양수여야 한다. (음수 불가능)
기본 사용방법
GET /_search
{
"query": {
"script_score": {
"query": {
"match": { "message": "elasticsearch" }
},
"script": {
"source": "doc['my-int'].value / 10 "
}
}
}
}
파라미터
- query: 쿼리 구문
- script: score를 계산할 스크립트
- min_score(Optional): 검색 결과에 포함할 최소 스코어
- boost: 가중치 (기본은 1.0)
predefined functions
painless 스크립트에 미리 정의되어 있는 functions.
직접 구현하는 것보다 predefined를 사용하는 것이 좋다고 한다. (내부 메커니즘에 의해 더 효율적이라고 함)
- Saturation
- Sigmoid
- Random score function
- Decay functions for numeric fields
- Decay functions for geo fields
- 거리 기반 점수에서, 특정 범위가 넘어갈 때마다 더 낮은 점수(가중치)를 주려면 이걸 사용하면 된다.
- Decay functions for date fields
- Functions for vector fields
Pinned Query
Function score
- 필터링된 문서 내에서 스코어를 계산한다.
- function_score를 사용하기 위해서는 쿼리와 function을 미리 정의해두어야 한다.
- 여러 가중치를 두고 사용하기에 가장 적절해보인다.
- 각 스코어들의 값 범위가 다르다.
- 이 범위를 조정하기 위해 weight를 function마다 조정하여야 한다.
기본 사용방법
# 가장 기본 사용방법
GET /_search
{
"query": {
"function_score": {
"query": { "match_all": {} },
"boost": "5",
"random_score": {},
"boost_mode": "multiply"
}
}
}
# functinos combile
GET /_search
{
"query": {
"function_score": {
"query": { "match_all": {} },
"boost": "5",
"functions": [
{
"filter": { "match": { "test": "bar" } },
"random_score": {},
"weight": 23
},
{
"filter": { "match": { "test": "cat" } },
"weight": 42
}
],
"max_boost": 42,
"score_mode": "max",
"boost_mode": "multiply",
"min_score": 42
}
}
}
스코어 계산 방식
- score_mode: 각 스코어들이 어떻게 결합(combine)될 것인지
- multiply: 스코어들 간의 곱셈 (default)
- sum
- avg
- first
- max
- min
- boost_mode: score_mode를 통해 계산된 하나의 스코어가 쿼리의 스코어(유사도 스코어)와 어떻게 결합될 것인지
- multiply
- replace: 쿼리 스코어를 사용하지 않고 대체한다.
- sum
- avg
- max
- min
지원하는 스코어 계산 방식
- script_score
- 스크립트 표현식으로 score를 계산하는 방법
- 반드시 0 또는 양수로 표현해야 한다.
- 스크립트는 빠른 실행을 위해 컴파일 후에 캐싱된다. (재사용을 위해서는 변수는 params를 사용한다)
- Random
- 랜덤으로 값을 생성하는 방식
- Field Value factor
- 필드 값 기반으로 가중치나 변환 로직만 적용하는 방식
- script를 사용할 때의 오버헤드를 피할 수 있다.
- Decay functions
- 숫자, 좌표, 날짜 값에 대해 특정 거리를 넘어설 때마다 감쇠하여 점수를 계산하는 방식
- 여러 필드를 같이 사용할 수도 있다.
- 지원하는 decay functions
- gauss
- exp
- linear
# 기본 사용 방법
"DECAY_FUNCTION": {
"FIELD_NAME": {
"origin": "11, 12", # 기준 위치
"scale": "2km", # 감쇠함수가 적용되는 단위
"offset": "0km", # offset 이후부터만 decay 적용
"decay": 0.33 # scale마다 decay*score 값이 적용될 수 있도록 만든다.
}
}
# gauss를 사용한 예시
GET /_search
{
"query": {
"function_score": {
"gauss": {
"@timestamp": {
"origin": "2013-09-17",
"scale": "10d",
"offset": "5d",
"decay": 0.5
}
}
}
}
}
랭킹 스코어 구현하기
- 리서치한 결과, 여러 스코어를 가중치를 두고 곱할 수도 있으며 predefined function과 decay function을 사용할 수 있는 Function Score 방식이 적절해보인다.
- 필요한 스코어들을 구현해보자.
유사도(similarity)
- default로 BM25의 유사도를 사용한다.
- _score로 나오는 값인데, 값이 0~1 사이가 아니기 때문에 어떻게 조정할 것인지가 중요하다.
- sigmoid(로지스틱 함수)를 적용하는 건 어떨까?
"functions": [
{
"script_score": {
"script": {
"source": "1 / (1 + Math.exp(-1 * _score))"
}
}
}
]
- 값의 범위 문제
- 다만 logistic function은 0일 때 0.5, 음수일 때 0~0.5가 나온다.
- _score는 유사도 기반의 BM25 스코어이기 때문에 반드시 양수만 나온다.
- 즉, 위의 로직은 반드시 0.5 ~ 1사이만 나온다.
- 이것을 해결하기 위해 score*2-1을 해서 0~1사이로 바꿔보는 방법도 있다.
- 다만 점수의 분포가 달라진다. max 값을 설정하는 방법도 있을 듯함.
"functions": [
{
"script_score": {
"script": {
"source": "2 / (1 + Math.exp(-1 * _score)) - 1"
}
}
}
]
# _score를 상수로 바꿔서 돌려보면 0~1 사이로 잘 나온다.
거리 계산(distance)
- 근접도를 계산하는 방법은 몇 가지 있다.
- planeDistance
- Euclidean distance, 두 좌표의 직선거리를 계산하는 방식
- 거리가 멀어질수록 부정확하다.
- arcDistance
- great-circle distance, 지구를 구체로 두고 계산하는 방식
- 더 정확한 거리를 계산할 수 있지만 속도가 느리다.
- decay function
- https://github.com/elastic/elasticsearch/blob/main/server/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java
- arcDistance를 쓰는 것으로 보인다.
- offset과 scale을 두고 거리마다 가중치를 부여할 수 있다.
- planeDistance
- decay function을 이용해보자.
- 어떤 function을 쓰는 것이 좋을까?
- 가까운 거리라면 가중치를 크게 두지 않고, 먼 거리일수록 가중치가 떨어지는 것이 좋아보인다.
- gauss를 사용해보자.
"functions": [
{
"gauss": {
"location": {
"origin": {
"lat": 37.3,
"lon": 126.8
},
"scale": "5km",
"offset": "0km",
"decay": 0.5
}
}
}
]
최근성(recency)
- 거리 계산과 동일하게 decay를 사용해도 될 듯.
"functions": [
{
"linear": {
"@timestamp": {
"origin": "now",
"scale": "120d",
"offset": "30d",
"decay": 0.5
}
}
}
]
퀄리티(Quality)
쿼리와 별개로 문서 자체에 랭킹을 부여하기 위한 요소.
특정 필드명으로 점수를 부여하고, 랭킹 조정을 위한 쿼리에서 해당 필드의 점수를 사용하는 방식
다음 글에서 좀 더 자세히 알아보자
가중치 부여하기
- 가중치를 어떻게 부여하나?
- weight에 가중치를 설정할 수 있다.
- 가중치를 어떻게 관리하나?
- 가중치가 바뀔 수 있기에, ES 스크립트에서 가중치를 고정하는 것보다는 ES를 사용하는 API 단에서 가중치를 조정하자.
- 예시로 복합 랭킹 점수를 쿼리로 구현해보자
- 랭킹은 아래의 요소들을 가정하고 만들었다.
- 거리 점수: 문서(장소 데이터)의 위치와 사용자의 위치가 멀수록 점수가 낮아진다.
- 유사도 점수: 쿼리의 BM25 점수에 정규화를 씌운 점수 (정규화를 이렇게 하는게 적절한지는 좀 더 알아봐야할 듯
- 최신성 점수: 문서의 수정일자가 오래될수록 점수가 낮아진다.
"functions": [
{
"script_score": {
"script": {
"source": "2 / (1 + Math.exp(-1 * _score)) - 1"
}
},
"weight": 0.33
},
{
"gauss": {
"location": {
"origin": {
"lat": 37.3,
"lon": 126.8
},
"scale": "5km",
"offset": "0km",
"decay": 0.5
}
},
"weight": 0.33
},
{
"linear": {
"@timestamp": {
"origin": "now",
"scale": "120d",
"offset": "30d",
"decay": 0.5
}
},
"weight": 0.33
}
],
"score_mode": "sum",
"boost_mode": "replace"
정리
랭킹을 구현하기 위해 ES에서 스크립트를 어떻게 구현하면 되는지를 먼저 알아봤다.
예시에서는 거리, 유사도, 최신성을 섞어서 랭킹 점수를 만들어봤지만... 실제로 랭킹을 구성하기 위해 어떤 요소들이 있고 어떻게 사용할 수 있을지를 다음 글에서 좀 더 알아보자.
퀄리티 점수, 근접성(Proximity) 점수 등도 같이 알아보고, 예시로 ES 검색 랭킹을 구현해보는 것을 목표로 한다.