훈스비
개발잡부
훈스비
전체 방문자
오늘
어제
  • 분류 전체보기 (35)
    • 스터디 (29)
      • 데이터 분석 (4)
      • SQL (4)
      • Python (1)
      • JavaScript (1)
      • Spark (1)
      • DevOps (7)
      • 기타 (8)
      • 검색 엔진 (3)
    • 나의 이야기 (4)
      • 회사 생활 (3)
    • 사이드 프로젝트 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • Docker
  • elasticsearch
  • flask
  • GCP
  • github copilot
  • helm
  • java
  • jupyterhub
  • k8s
  • Kubernetes
  • leetcode
  • linux
  • ncp
  • Ray
  • ReplicaSet
  • spark
  • spring
  • sql
  • udemy
  • Virtualization
  • vm
  • vsCode
  • vue
  • 개발원칙
  • 검색엔진
  • 데이터 분석
  • 랭킹
  • 백엔드
  • 사이드프로젝트
  • 유사도 정규화

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
훈스비

개발잡부

카테고리 없음

[검색엔진] Elasticsearch에서의 랭킹 (1) - 스크립트

2024. 2. 18. 20:41

개요

검색엔진에서 랭킹은 상당히 중요하다.

검색 결과가 10개 수준인 경우 랭킹이 잘못되어도 품질에서 차이가 크게 안 날 수 있지만, 검색 결과가 수 천, 수 만개인 경우 품질에 상당한 차이가 발생된다.

ES에서 랭킹을 어떻게 조정할 수 있는지, 랭킹에는 어떤 요소들이 있는지 등을 살펴본다.

우선 첫 번째로 ES에서 스크립트를 작성하는 방법부터 알아보자.

 

ES에서 스크립트 작성하기


  • ES에서 script_score를 이용해서 커스터마이징 랭킹을 구현할 수 있다.
  • script_score는 script 언어를 이용해서 코드를 작성하는 방식이다.
  • script 언어는 painless, expression, mustache, java를 지원한다.
    • Scripting
    • 이전에는 python, javascript를 지원하였으나.. 5.x버전부터 지원하지 않는다.
    • 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으로 결과를 미리 생성하여 문서에 포함시키는 것으로 속도를 향상시킬 수 있다.
    • ingest pipeline
    • script processor
  • 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을 두고 거리마다 가중치를 부여할 수 있다.
  • 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 검색 랭킹을 구현해보는 것을 목표로 한다.

저작자표시 비영리 변경금지 (새창열림)

    티스토리툴바