글을 시작하기 앞서 이 블로그는 Elasticsearch(이하 ES)에 관한 글이지만, ES와의 통신을 python을 이용해서 하는 것이므로 url을 이용한 query를 공부하고자 하는 분에게는 크게 도움이 안될 수 있다는 점을 미리 알려드린다. 물론 필자는 url query를 공부해서 적용한 케이스라 반대 경우도 충분히 가능하다고 본다는 점에서는 만약 manual/specific order로 sorting하는 방법을 찾는 분이라면 무의미하지는 않을 것이라 본다.

참고로 필자는 elasticsearch-dsl을 사용해서 구현했다.

About

필자가 최근에 ES를 cache layer로써 사용하기 시작하면서 시간을 제법 빼앗겼던 작업에 대해서 오늘은 공유하고자 한다. 필자는 추천시스템으로부터 A라는 유저에게 추천할 상품 리스트를

pks = [4882, 1061, 7324, 6471, 3587]

와 같은 형태로 받게된다. 그러면 이 리스트를 ES의 Index에 query를 보내고, sorting을 위의 순서대로 return받고 싶었는데 그런 기능을 찾는 것이 쉽지않았다. 이러한 기능을 설명한 글을 찾는것도 생각보다 제한적이었으며, 있다고 해도 특별한 설명없이 코드만 있어서 필자의 상황에 적용하는 것에 공을 들여야했다. 부디 이 글이 다른 분들에게 도움이 되길 바란다.

Script based sorting

Manual order(우리가 지정해준 순서)대로 값을 return받는 것은 ES가 default로 제공하는 기능이 아니므로, 우린 script based sorting이라는 방법을 사용해야한다. ES는 사용자정의 script를 이용해서 sorting logic을 만들 수 있게 지원하며, 이 script에는 여러가지 언어를 이용할 수 있다. 자세한 내용은 링크를 참고하면 되고, 이 블로그에서는 ES에서 가장 추천하는 언어인 Painless를 사용해서 설명하도록 하겠다.

Implementation

먼저 Index의 mapping을 살펴보자.

{
  "my_index": {
    "mappings": {
      "doc": {
        "properties": {
          "item_id": {
            "type": "long"
          },
        }
      }
    }
  }
}

이제 코드로 구현하는 법을 살펴보겠다.

import elasticsearch
from elasticsearch_dsl import Search

es = elasticsearch.Elasticsearch('localhost:9200')

pks = [4882, 1061, 7324, 6471, 3587]
scores = dict(zip(pks, range(len(pks))))
"""
{4882: 0,
 1061: 1,
 7324: 2,
 6471: 3,
 3587: 4}
"""

param = {
    '_script': {
        'type': 'number',
        'script': {
            'lang': 'painless',
            'params': {
                'scores': scores
            },
            'source': 'return params.scores[Long.toString(doc.item_id.value)]',
        },
        'order': 'asc'
    },
}

s = Search(using=es, index='index_name')
s = s.query('terms', cosmetic_id=pks).sort(param)
s = s[:s.count()]

위에서부터 살펴보자면,

  1. es라는 변수명으로 Elasticsearch와 연결하는 객체를 만든다.
  2. 이 부분이 이 블로그의 전부다. script를 만들어서 파라미터로 넣어주게 되는데, 하나하나 살펴보자.

    • 먼저 ‘_script’ key는 ES에서 정한 것이므로 그대로 따르면 된다.
    • type은 우리가 원하는 결과를 위해서는 number를 해야한다.
    • script 부분을 살펴보자.

      • script의 value도 dictionary형태로 넣어주며, key로 사용된 값들은 전부 정해진 문법이니 그대로 따르면 된다.
      • lang에는 위에서 설명한대로 painless를 이용하겠다(이 외에는 위의 링크로 확인하자).
      • source에 넣은 값이 search 결과를 score를 기반으로 sorting하기 위한 로직이다. 위에서 params로 넣어준 scores를 이용하게된다. 문법을 살펴보면

        return params.scores[Long.toString(doc.item_id.value)]

        형식이다. 중요한 부분은 doc이다. doc으로 들어와있는 값이 바로 search를 통해서 찾은 값들이 들어있다. 위의 mapping에서 알 수 있듯이, item_id는 타입이 long타입이다. 그래서 Long.toString()을 이용해서 문자열로 변환해준 후에 scores의 value에 접근한 것이다. 여기서 필자는 가장 많은 시간을 낭비했는데, 그 이유는 이렇다. 처음에는 doc._id.value를 이용해서 위의 방법을 사용했을 때는 문제없이 값이 잘들어왔었다. 하지만 좀 더 속도면에서나 자원적인면에서 최적화를 위하여 item_id를 key값으로 사용해서 정렬하려고하자 문제가 생기기 시작한 것이다. 먼저는 scores dictionary의 key값을 int로 변경하면 되겠거니 하고 도전했는데 이상하게 empty list를 반환하거나 오류가 나거나 했다. 그 이유는 바로 scores의 key값을 우리가 어떤 type으로 넣어주던지 ES가 받아서는 String으로 변경해서 사용한다는 것이다. 그래서 우리가 선택할 수 있는 방법은 값을 String으로 형변환시켜주는 것이다. 여기서 item_id는 long타입으로 들어가 있었기 때문에 Long클래스를 이용해서 형변환했다. 어쨋든, 이렇게 형변환후에 return하는 값은 위에서 확인할 수 있듯이 우리가 원하는 리스트 순서대로 0부터 오름차순으로 이루어진 값이 된다.

      • 이제 그렇게 return받은 값을 이용해서 오름차순으로 반환해달라는 의미로 ‘order’: ‘asc’ 값을 넣어주면 된다.
  3. Search객체를 선언하고, query().sort()처럼 체인식으로 query를 날리면 된다. 마지막에 s[:s.count()]를 해준 것은 default로는 우리의 query에 적합한 결과가 몇개이든 5개를 return하기 때문에, list를 slice하는 방식으로 from, size를 지정해준 것이다.

Conculusion

예상보다는 ES에 관한 Tutorial이나 관련 문서들을 찾는 것이 쉽지않아서 고생을 했었다. 다음 편에서는 python의 elasticsearch-dsl패키지를 이용한 기본 사용법에 대해서 살펴보도록 하겠다.