본문 바로가기

Elastic

[Elastic] 검색 랭킹

검색을 통해 여러 문서의 결과를 돌려줄 때, 결과를 돌려주는 순서를 결정하는 것을 랭킹이라고 한다.
즉, 제일 검색 키워드와 관련있고 중요한 문서들을 정렬하여 먼저 돌려주는 것
  • 검색 결과의 Score 필드의 값으로 표현된다.
  • 분산적인 merge sort system -> 컬렉션 분석 시스템에서 제일 관련된 문서 Top 몇 가지를 가져와 merge/sort를 진행하고, 정렬/랭킹 시스템에서 병렬적인 가져온 문서들을 한 번더 merge/sort한다.
  • Linear한 스케일에 정렬할 수 있어 문서의 개수에 상관 없이 매우 빠른 속도로 제일 관련된 문서를 응답할 수 있다.

* 사용자가 키워드가 포함된 문서를 보고 싶은지, 키워드가 포함된 뉴스를 보고 싶은 것인지, 키워드가 많이 들어간 문서를 보고 싶은 것인지 등 하나의 키워드를 가지고 어떤 결과를 원하는 지 알기 힘들다. -> 추론을 통해 Score를 설정하여 응답한다.

  • 통합 검색 -> 인덱스 별(뉴스, 문서, 사전)로 나눠서 모든 결과 항목을 응답 결과를 줄 수 있다.
  • 사용자의 활동 이력에 뉴스를 많이 본 기록이 존재한다. -> 컬렉션 분석 시스템이 merge/sort하고 받은 결과에서 정렬/랭킹 시스템이 뉴스 키워드가 포함된 결과의 Score를 높여서 반환할 수 있다.
  • 다양한 방법으로 추론하여 응답 결과를 줄 수 있다.

 

Ranking: TF / IDF

  • TF: (Term-frequency) 단어 빈도 -> 특정 단어가 문서 내에서 얼마나 자주 등장하는지를 나타내는 값
    • 계산 식: sqrt(termFreq)
    • 높을수록 중요한 검색 결과이다.
  • IDF: (Inverse document frequency) 역문서 빈도 -> 한 단어가 문서 집합 전체에서 얼마나 공통적으로 나타나는지를 나타내는 값
    • 계산 식: 1 + ln(maxDocs / (docFreq + 1)
    • Stop word(the, a, ~한다) 등 중요하지 않은 단어에 적용한다.
    • 높을수록 중요하지 않은 검색 결과이다.

Elastic Search에서는 특별한 옵션을 설정하지 않아도 검색 결과에 TF/IDF가 영향을 준다.

 

랭킹 조절 방법 예제

1. 인덱스 생성

import requests
import json

url = "http://localhost:9200/products"

payload = json.dumps({
    "settings": {
        "index": {
            "number_of_shards": 1,
            "number_of_replicas": 1
        },
        "analysis": {
            "analyzer": {
                "analyzer-name": {
                    "type": "custom",
                    "tokenizer": "keyword",
                    "filter": "lowercase"
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "id": {
                "type": "long"
            },
            "content": {
                "type": "text"
            },
            "title": {
                "type": "text"
            },
            "url": {
                "type": "text"
            },
            "image_file": {
                "type": "text"
            },
            "post_date": {
                "type": "date"
            },
            "modified_date": {
                "type": "date"
            },
            "shipped_from": {
                "type": "text"
            },
            "local_confidence": {
                "type": "rank_features"
            }
        }
    }
})
headers = {
    'Content-Type': 'application/json'
}

response = requests.request("PUT", url, headers=headers, data=payload)

print(response.text)

* local_confidence라는 필드를 rank_features 타입으로 설정

** rank_features: 랭킹을 위해서만 존재하는 메타데이터, 해당 값을 활용해서 boost, demotion을 진행한다.

검색할 때, 쿼리 파라미터로 추가하면 해당 필드가 검색 결과 랭킹에 영향을 준다.

 

2. Elastic Search에 데이터 덤프

import ast
import datetime
import json
import mysql.connector
import requests

def getPostings():
    cnx = mysql.connector.connect(user='root',
                                password='비밀번호',
                                host='localhost',
                                port=9906,
                                database='데이터 베이스')
    cursor = cnx.cursor()

    query = ('쿼리')
    cursor.execute(query)

    posting_list = []
    for (id, content, title, url, post_date, modified_date, meta_value, image) in cursor:
        print("Post {} found. URL: {}".format(id, url))

        shipping_location = assumeShippingLocation(meta_value)
        ships_local = 0.1
        ships_intl = 0.1
        if (shipping_location == '국내'):
            ships_local = 1.0
        else:
            ships_intl = 1.0
        local_confidence = { 'KR': ships_local, 'intl': ships_intl }
        product = ProductPost(id, content, title, url,
                              post_date, modified_date, shipping_location, image, local_confidence)
        posting_list.append(product)

    cursor.close()
    cnx.close()
    return posting_list
    
# 엘라스틱서치에 출력하는 함수
def postToElasticSearch(products):
    putUrlPrefix = 'http://localhost:9200/products/_doc/'
    headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
    for product in products:
        id = str(product.id)
        print(id)
        r = requests.put(putUrlPrefix + id, data=json.dumps(product.__dict__,
                         indent=4, sort_keys=True, default=json_field_handler), headers=headers)
        if r.status_code >= 400:
            print("There is an error writing to elasticsearch")
            print(r.status_code)
            print(r.json())

# 출고지
def assumeShippingLocation(raw_php_array):
    if u'국내' in raw_php_array:
        return '국내'
    return '해외'

# datetime -> iso 형식
def json_field_handler(x):
    if isinstance(x, datetime.datetime):
        return x.isoformat()
    raise TypeError("Unable to parse json field")

# 제품 페이지를 표헌하는 class
class ProductPost(object):

  def __init__(self, id, content, title, url, post_date, modified_date, shipped_from, image_file, local_confidence):
    self.id = id
    self.content = content
    self.title = title
    self.url = url
    self.post_date = post_date
    self.modified_date = modified_date
    self.shipped_from = shipped_from
    self.image_file = image_file
    self.local_confidence = local_confidence

p = getPostings()
postToElasticSearch(p)
  • meta_value 값이 국내이면, ships_local = 1.0, 해외이면 shops_intl = 1.0으로 변경하고 local_confidence 필드에 값을 넣는다.

3. 저장 결과 확인

http://localhost:9200/products/_search

 

위와 같이 shipped_from이 해외이면 intl = 1, shipped_from이 국내이면 KR=1로 저장된 것을 확인할 수 있다.

 

4. 검색(https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html)

http://localhost:9200/products/_search?q=

 

* 위와 같은 통합 검색으로는 랭킹에 영향을 주는 필드를 설정할 수 없다.

 

위와 같은 방법으로 쿼리를 요청해야 한다.

검색 키워드 : 장미

boost를 1.0으로 주어 local_confidence.KR 값 순으로 가져온다.

{
    "took": 20,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.9281977,
        "hits": [
            {
                "_index": "products",
                "_type": "_doc",
                "_id": "25",
                "_score": 1.9281977,
                "_source": {
                    "content": "국내에서 배송되는 흰색 장미와 부케입니다.",
                    "id": 25,
                    "image_file": "2021/07/white_rose_bouquet.jpg",
                    "local_confidence": {
                        "KR": 1.0,
                        "intl": 0.1
                    },
                    "modified_date": "2021-07-02T15:27:29",
                    "post_date": "2021-07-02T14:16:03",
                    "shipped_from": "국내",
                    "title": "흰색 장미 부케",
                    "url": "http://localhost:8000/?post_type=product&p=25"
                }
            },
            {
                "_index": "products",
                "_type": "_doc",
                "_id": "18",
                "_score": 1.4120991,
                "_source": {
                    "content": "크고작은 핑크색 장미들로 부케를 만들어 보았습니다.",
                    "id": 18,
                    "image_file": "2021/07/pink_rose_bouquet.jpg",
                    "local_confidence": {
                        "KR": 0.1,
                        "intl": 1.0
                    },
                    "modified_date": "2021-07-02T15:27:42",
                    "post_date": "2021-07-01T19:05:02",
                    "shipped_from": "해외",
                    "title": "핑크빛 장미 부케",
                    "url": "http://localhost:8000/?post_type=product&p=18"
                }
            }
        ]
    }
}

위와 같이 KR이 더 높게 설정된 데이터의 Score 값이 더 높게 측정되고 먼저 출력되는 것을 확인할 수 있다.

* boost의 값이 1보다 작으면 오히려 해당 필드의 값이 낮을 수록 더 높은 Score가 도출된다.

 

** rank_feature에서 boost 필드가 아닌 다양한 필드를 사용할 수 있다.

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-rank-feature-query.html (참조)

 

'Elastic' 카테고리의 다른 글

[Elastic] 지식 그래프란?  (0) 2022.10.06
[Elastic] Elastic Search란?  (1) 2022.10.05
[Elastic] 검색 엔진이란?  (0) 2022.10.01