엘라스틱 서치로 이미지 검색 백엔드 만들기
2020년 02월 28일 작성TL;DR
코드는 여기1
Elasticsearch 7.3+ 부터 지원되는 dotProduct
또는 consineSimiarity
를 사용하여 유사도를 계산한다.
(dense_vector
는 7.0 부터 지원되었지만 위의 기능들이 없기 때문에 플러그인이나 직접 구현을 통해 사용해야한다.)
시작하며
몇년 전에 이미지를 입력하면 동일한 상품 목록을 가져오는 프로젝트를 한 적이 있다.
이미지에서 SIFT를 뽑고 LSH 로 해싱한 뒤, 해당 결과를 Elasticsearch(이하 ES) 에 쌓아서 동일한 상품 검색을 구현했다.
동일한 상품은 잘 찾지만 비슷한 상품을 검색하는 것에 있어서 SIFT 피쳐의 성능은 상당히 떨어졌었다.
이번에 진행한 프로젝트에서는 동일한 상품 뿐만 아니라 비슷한 상품도 잘 찾아야 했는데, 입력값의 피쳐의 분포를 보니 cosineSimiarity 가 가까울 수록 비슷한 상품이라는 특성을 가지고 있었다.
대량의 모집단에 대해서 consineSimiarity 를 빠르게 수행할 수 있는 툴로 예전에 썼던 ES가 생각나서 사용했 고 결과가 나쁘지 않았다.
본 글에서는 ES
에서 dotProduct
를 사용하는 방법을 알아본다. 단, 위에서 설명한 작업에서는 dotProduct 와 consineSimiarity 는 사실상 같은 역할을 하기 때문에 앞으로 dotProduct
로 표현을 통일하겠다. 타이핑 하기 편함
요구사항
ES 에서 dotProduct 를 사용하려면 dense_vector
타입으로 인덱스의 필드가 매핑되어 있어야 한다.
dense_vector
타입은 ES 7.x 에서 지원된다.2 문서에는 7.x 로 퉁쳐져서 모든 기능이 지원되는 것처럼 나오지만, dotProduct
함수는 7.3+ 에서만 지원된다.3
따라서 7.5.1
버전을 설치해서 진행하는 것을 추천한다. 사용할 파이썬 라이브러리 최신이 7.5.1 이라서
Elasticsearch 설치
제일 쉬운방식은 역시 도커로 설치하는 것이다.
$ docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.5.1
이후 curl 을 날려서 확인해보면 잘 뜨는 것을 확인할 수 있다.
$ curl -X GET http://localhost:9200
{
"name" : "1c993be83823",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "-RlyuqqhTEKY9N2StM-WjA",
"version" : {
"number" : "7.5.1",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "7f634e9f44834fbc12724506cc1da681b0c3b1e3",
"build_date" : "2020-02-06T00:09:00.449973Z",
"build_snapshot" : false,
"lucene_version" : "8.4.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
인덱스 생성 및 매핑
ES 는 인덱스에 대한 POST 요청시 인덱스가 없으면 알아서 필드를 판단해서 인덱스를 생성하고 매핑해준다.
하지만 vector 형태의 데이터를 밀어넣으면 double 형태로 구성을 하기 때문에 dotProduct 가 동작하지 않는다. 또한 double 형태의 필드에 대해 직접 dot을 구현해서 처리하려고 해도, 필드 인덱싱을 해버리기 때문에 원하는 결과를 얻을 수 없다. (즉, [.4, .1, .2, .3] 을 넣으면 [.1, .2, .3, .4] 로 정렬해서 저장한다.)
프로덕션에서도 모종의 이유로버그?! 동일한 필드에 숫자, 문자가 섞여서 들어오는 경우도 있기 때문에 보통은 인덱스를 먼저 매핑해두고 사용하는 것이 안전하다.
python을 이용하여 인덱스를 매핑해보자.(curl 로 해도 되지만 재사용을 위해서) 먼저 ES 버전에 맞는 클라이언트 라이브러리를 설치한다.
$ pip install elasticsearch==7.5.1
이후 로컬호스트에 설치된 ES 에 인덱스를 생성해준다. 여기서는 features
라는 이름의 인덱스를 생성하고, 인덱스의 feature
필드를 dense_vector
로 설정해준다.
dims
에서 해당 벡터의 차원수를 입력해줘야하는데 다차원 지원은 하지 않기 때문에, 다차원 데이터를 인덱싱할 땐 flatten 작업을 해야한다. 여기서는 128 차원으로 입력했다.
from pprint import pprint
from elasticsearch import Elasticsearch
es = Elasticsearch(hosts=['http://localhost'], port='9200')
es.indices.create(index='features', body={
"mappings": {
"properties": {
"feature": {
"type": "dense_vector",
"dims": 128,
},
"image_id": {
"type": "text"
}
}
}
}, ignore=400)
인덱스가 잘 생성되었는지 확인해본다.
pprint(es.indices.get(index='features'))
인덱스 쿼리
생성한 인덱스에 dotProduct 를 이용하여 쿼리를 해보자.
import random
from pprint import pprint
from elasticsearch import Elasticsearch
es = Elasticsearch(hosts=['http://localhost'], port='9200')
res = es.search(index='features', body={
'query': {
'script_score': {
'query': {
'match_all': {}
},
'script': {
'source': "dotProduct(params.query_vector, doc['feature']) + 1.0",
'params': {
'query_vector': [random.gauss(0, 0.432) for _ in range(128)],
}
}
}
}
})
pprint(res['hits']['hits'])
현재는 데이터가 없기 때문에 빈 리스트가 보이겠지만 에러가 없다면 성공이다.
피쳐를 넣고 뽑는 것은 글의 범위를 벗어나기 때문에 귀찮고 쉽다 생략한다.
마치며
dotProduct, consineSimialrity 같은 쿼리를 쉽게 할 수 있는 분산 저장소가 딱히 없어서 고민이었는데 ES 에서 지원해줘서 정말 편안하게 개발할 수 있다.
하지만 Amazon Elasticsearch 는 아직 7.1 까지 밖에 지원을 안하고 있어서4 AWS 와 연동해서 쓰려면 ECS EC2 등으로 직접구성해서 써야한다는 점이 아쉽다.
현재 열심히 건의하고 있으니 곧 지원해주지 않을까…