ES 검색엔진 만들기 (3) 스코어링
(230211 수정)
ES 검섹앤진 만들기 (3) 스코어링 해보기
검색엔진을 만들때, 어떠한 검색 결과가 우선순위가 높을지에 대해 컨트롤 하는 기술은 굉장히 중요하다. 이러한 스코어링에 필요한 기초 스킬들을 익혀보고, 사용해보도록 하자
차례
- 정확도 (Relevance)
- boost
- explaination
- function score
- 스코어 결합하기
1. 정확도 - Relevance
- RDBMS는 단순히 참/거짓만 판단할 뿐 각 결과가 얼마나 정확한지에 대한 판단이 불가능하다.
- ES 와 같은 FULL TEXT SEARCH ENGINE은 검색 조건과 얼마나 정확하게 일치하는 지 를 계산하는 알고리즘을 가지고 있다.
- 정확도를 기반으로 사용자가 가장 원하는 결과를 먼저 보여줄 수 있다.
- 즉, 검색하여 찾은 결과 중 사용자가 입력한 검색어와의 연관성을 계산해 정확도가 높은 순으로 출력한다,
elasticsearch Score
검색된 결과가 얼마나 검색 조건과 일치하는지를 나타낸다. 다음과 같은 요청을 보냈을 때 결과를 보도록 한다.
Elasticsearch는 기본적으로 BM25 알고리즘을 이용해 문서에 대한 SCORE를 계산한다. TF(Tern Frequency) 도큐먼트 내에 검색된 텀이 많을수록 점수가 높아진다. (도큐먼트 내에서 중복되는 검색어)
IDF(Inverse Docment Frequency) 여러 검색어 중에서 전체 검색 결과에 희소하게 나타나는 단어일수록 중요한 텀일 가능성이 높다. 따라서 검색한 텀을 포함한 도큐먼트가 많을 수록 해당 텀이 가지는 점수가 감소한다.
Field Length 필드 길이가 짧은 필드에 있는 텀의 비중이 크다. 블로그 포스트를 검색하는 경우 검색하려는 단어가 제목__과 __내용 필드에 모두 있는 경우 텍스트 길이가 긴 내용 필드 보다는 텍스트 길이가 짧은 제목 필드에 검색어를 포함하고 있는 블로그 포스트가 점수가 높게 나타난다.
2. BOOST
2-1. boosting?
엘라스틱서치의 Boosting 기능은 검색 결과의 relevance score를 컨트롤 할수 있게 해주는 가장 간편하고, 직관적인 방법이다.
boosting 은 match, term, range 쿼리 등에서 사용이 가능하고, 또한 특정 필드에 부스팅을 적요하여 특정 필드를 다른 필드보다도 우순순위를 높게 지정할 수도 있다.
검색어 마다 부스팅 weight을 지정하여 특정 키워드에 가중치를 더 부여하는것 또한 가능하다.
elasticsearch v5.0 까지만 해도 엘라스틱 서치에는 query-time boosting 과 index-time boosting 이 존재했지만, 현재는 query-time boostring 만 남아있다.
2-2. query time boosting
위에서 말했듯 boosting 사용 방법은 매우 간단하다.
검색을 할때, 쿼리에 boost 매개변수만 추가하면 된다. 부스트 파라미터 값은 0에서 무한대까지이며, 값이 클수록 중요도가 높음을 나타낸다.
GET poi/_search
{
"query": {
"match": {
"name": {
"query": "첨단 휴먼시아"
}
}
}
}
response
{
...
"_index" : "parrot_address_230210",
"_type" : "_doc",
"_id" : "8gdgOIYBQl5FG8mVWnOU",
"_score" : 20.85457,
}
GET poi/_search
{
"query": {
"match": {
"target_poi_name": {
"query": "휴먼시아",
"boost": 2
}
}
}
}
response
{
...
"_index" : "parrot_address_230210",
"_type" : "_doc",
"_id" : "8gdgOIYBQl5FG8mVWnOU",
"_score" : 41.70914,
}
이럴떈 정확히 2배의 점수가 부여된걸 볼수 있다.
2-3. 필드별 부스팅 vs 용어별 부스팅
2-3-1. 필드별 부스팅
POST poi/_search
{
"query": {
"multi_match": {
"query": "신당동 떢볶이",
"fields": ["poi_name^3", "dong_name"]
}
}
}
poi_name 필드가 dong_name 보다 3배 정도 중요하다고 생각하면 된다.
2-3-2. 용어별 부스팅
POST poi/_search
{
"query": {
"match": {
"query": "신당동^3 떡볶이"
}
}
}
신당동 검색어가 떡볶이 검색어보다 3만큼 부스팅 시켰다.
정리: 결론적으로 Elastic seach boost 는 검색 결과의 관련성 점수를 미세하게 조절이 가능한 기능으로, 특정 키워드에 가중치를 부여하거나 특정 필드의 우선순위를 지정하려는 경우 적절히 사용할수 있다.
3. explain=true
실제 검색 요청시 explaion=true 를 Query Param으로 넘기면 score가 어떻게 되었는지 자세히 확인할 수 있다.
요청
GET parrot_address/_search?explain=true
{
"query" :
{
"size": 1,
"match": {
"name": "푸르지오"
}
}
}
결과
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 7,
"relation" : "eq"
},
"max_score" : 15.110407,
"hits" : [
{
"_shard" : "[parrot_address][0]",
"_node" : "OUY33w3WTZWeb5r8Rj6i1w",
"_index" : "parrot_address",
"_type" : "_doc",
"_id" : "jwwR230B5gETIct5_CkS",
"_score" : 15.110407,
"_source" : {
"sido_name" : "광주광역시",
"gu_name" : "남구",
"dong_name" : "봉선동",
"road_name" : "유안초등북1길",
"li_name" : "",
"lot_number_address" : "광주광역시 남구 봉선동 223",
"road_number_address" : "광주광역시 남구 유안초등북1길 2-12",
"pnu_code" : 2915511500102230000,
"name" : "푸르지오"
},
"_explanation" : {
"value" : 15.110407,
"description" : "weight(name:푸르지오 in 3146) [PerFieldSimilarity], result of:",
"details" : [
{
"value" : 15.110407,
"description" : "score(freq=1.0), computed as boost * idf * tf from:",
"details" : [
{
"value" : 2.2,
"description" : "boost",
"details" : [ ]
},
{
"value" : 9.482655,
"description" : "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details" : [
{
"value" : 7,
"description" : "n, number of documents containing term",
"details" : [ ]
},
{
"value" : 98474,
"description" : "N, total number of documents with field",
"details" : [ ]
}
]
},
{
"value" : 0.7243084,
"description" : "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details" : [
{
"value" : 1.0,
"description" : "freq, occurrences of term within document",
"details" : [ ]
},
{
"value" : 1.2,
"description" : "k1, term saturation parameter",
"details" : [ ]
},
{
"value" : 0.75,
"description" : "b, length normalization parameter",
"details" : [ ]
},
{
"value" : 1.0,
"description" : "dl, length of field",
"details" : [ ]
},
{
"value" : 11.162469,
"description" : "avgdl, average length of field",
"details" : [ ]
}
]
}
]
}
]
}
}
]
}
}
위 검색 결과에서 _explanation 부분에서 확인이 가능하다.
BM25 알고리즘에서 문서 점수 계산식 score = TF*tfNorm
즉, Elasticsearch 기본적으로 검색된 문서에 매칭된 키워드 수가 자주 반복될 수록, 또 평균 필드 길이보다 검색된 문서의 필드가 길수록 score가 올라간다.
기존 TF-IDF 보다 BM25가 더 정교한 SCORE 계산을 하기 떄문에 ES버전 6.3 부터 해당 알고리즘이 사용되었다.
Elasticsearch 점수 계산을 변경하는 방법을 알아보자, 단순 용어에 대한 빈도수와 필드를 부스팅하여 스코어를 조정하는 것은 한계를 가지고 있다. 좀 더 유연하게 점수를 조절할 수 있는 방식으로 function_score 가 있다.
만약, 검색 요청 시 sort 필드를 사용하게 되면 따로 score 점수가 반환되지 않는다.
sort 필드 사용하며 score 계산을 null로 반환되는것을 방지하기 위해선 "track_scores":true 를 지정해 주면 된다.
4. Function Score
아무런 스코어링도 하지 않은 function_score 쿼리 예시는 아래와 같다.
{
"query":
{
"function_score": {
"query": {
"match": {
"description": "직장인"
}
},
"functions": []
}
}
}
“functions”에 정의될 함수들은 쿼리의 결과에 대해서만 적용이 된다.
-
boost_factor
- 가잔 간단한 함수 - 단순 상수를 곱하여 계산한다.
- 필터를 이용하여 부스팅할 문서를 결정
{
"query":
{
"function_score":
{
"query":
{
"match":
{
"description": "직장인"
}
},
"functions": [
{
"boost_factor": 1.5,
"filter": {"term": {"description": "연봉"}}
}
]
}
}
}
직장인 검색결과에서 description에 “연봉” 이라는 용어를 포함하고 있는 문서의 스코어에 1.5배를 한다.
-
filed_value_factor
- 숫자형 필드의 값을 스코어에 이용한다.
- 좋아요 버튼을 누른 카운트등을 검색 결과에 이용할때 사용한다.
{
"query":
{
"function_score":
{
"query":
{
"match":
{
"description": "직장인"
}
},
"functions": [
{
"field_value_factor":
{
"field": "review_count",
"factor": 2.5,
"modifier": "log"
}
}
]
}
}
}
_score=log(2.5Xdoc[‘review_count’].value)
-
3. script_score
가장 자유도가 높은 스코어링 방식
{
"query":
{
"function_score":
{
"query":
{
"match":
{
"description": "직장인"
}
},
"functions": [
{
"script_score": {
"script": "Math.log(doc[\"salaries\"].values.size() * myweight",
"params": {"myweight:2}
}
}
]
}
}
}
ES script는 다음에 알아보도록 한다.
-
radom_score문
- 문서를 랜덤하게 정렬하고 싶을때 사용
- seed값을 동일하게 주면 동일한 결과가 나타난다.
{
"query":
{
"function_score":
{
"query":
{
"match":
{
"description": "직장인"
}
},
"functions": [
{
"random_score": {
"seed": 1234
}
}
]
}
}
}
-
5. decay function
특정 필드의 값을 이용하여 스코어를 점진적으로 줄여 나가는 함수
- 시간이 오래된 정보일수록 스코어를 줄이기
- 거리상 먼 위치일수록 스코어를 줄이기
- 세가지 종류의 decay 함수들
- linear
- gauss
- exp 모든 decay 함수들은 아래와 같은 형식을 따른다.
{
"TYPE": {
"FIELD_NAME": {
"origin": "…",
"offset": "…",
"scale": "…",
"decay": "…"
}
}
}
offset: origin 으로 부터 스코어가 줄어들지 않는 구간의 거리 예를 들어, offset이 1km이면 origin으로 부터 1km 이내의 거리에서는 스코어 줄어들지 않는다. origin(reference): 함수곡선의 중심. 즉, 가장 스코어가 높은 지점 예를 들어 geo-location을 이용하는 경우 사용자의 현 위치가 origin이 된다. scale & dacay : 이 두값의 조합으로 스코어 값이 줄어드는 기준이 정해진다. 예를들어, scale의 값이 3km 이고 decay값이 0.3 이라면 origin 에서 부터 3km 멀어질수록 스코어가 0.3배로 줄어들게된다.
gussian decay 함수 예제
{
"query":
{
"function_score":
{
"query":
{
"match":
{
"description": "직장인"
}
},
"functions": [
{
"origin": "32.232112,127.324211",
"offset": "100m",
"scale": "2km",
"decay": 0
}
]
}
}
}
5. 스코어 결합하기
-
SCRORE_MODE
“functions” 배열안의 여러 함수의 스코들을 결합하는 방법
- multiply(default), first, sunm, avg, max, min
-
BOOST_MODE
함수의 스코어를 쿼리 결과의 스코어와 결합하는 방법
- multiply(defualt), replace, sum, avg, max, min