본문 바로가기

server/system design

Elasticsearch k-NN 알고리즘

자연어 처리(NLP) 시스템, 추천 엔진, 검색 기반 시스템과 같은 머신 러닝(ML) 애플리케이션을 구축할 때, 일정 수준 이상의 규모가 되면 k-Nearest Neighbor(k-NN) 검색을 활용된다. 데이터가 수억 개에서 수십억 개까지 늘어나면, k-NN 검색을 확장하는 것이 큰 도전 과제가 된다.

이러한 문제를 해결하기 위해 Approximate k-Nearest Neighbor (ANN) 검색이 등장했다. ANN은 k-NN 문제의 일부 제약을 완화함으로써 검색 속도를 획기적으로 줄일 수 있는 방법을 제공한다.

 

벡터 검색을 이해하는 쉬운 방법은 전통적인 어휘 검색(Lexical Search) 과 비교해보자. 우리가 익숙한 엘라스틱서치의 어휘 검색은 사용자가 입력한 단어나 그 변형(어간, 동의어 등)을 찾아 TF-IDF와 같은 알고리즘을 사용하여 색인된 데이터와 비교하는 방식이다. 반면, 벡터 검색(의미 검색)은 단어나 문장의 실제 의미를 파악하고, 의미적으로 유사한 항목을 찾는 데 초점을 맞춘다.

 

k-NN 검색은 주어진 데이터 집합에서 쿼리 벡터와 가장 가까운 k개의 벡터를 찾는 문제이다. 단순히 모든 벡터와 거리를 계산하여 상위 k개를 선택하는 방식은 이해하기 쉽지만, 데이터가 커질수록 확장성이 문제가 된다. 기본적인 k-NN 알고리즘의 검색 복잡도는 O(N log k) 로, 데이터가 수백만 개 이상이 되면 속도가 급격히 저하된다.

 

ANN 검색은 이러한 한계를 극복하기 위해 색인 시간을 늘리고, 검색 시 더 많은 메모리를 사용하며, 완전한 정확성 대신 근사값을 반환하는 방식을 채택한다. 이를 통해 검색 속도를 획기적으로 개선할 수 있으며, 여러 가지 알고리즘을 활용하여 구현 되어 있다.

 


 

1. 브루트포스 (Exact kNN)

기본적으로 dense_vector 필드를 설정하면 브루트포스(Exact kNN, Exhaustive Search) 를 사용할 수 있다.
이 방식은 모든 벡터를 순차적으로 비교하여 가장 유사한 결과를 찾는 방식으로, 정확도는 높지만 검색 속도가 느릴 수 있다.

PUT my_index
{
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "dense_vector",
        "dims": 128,
        "index": true,
        "similarity": "l2_norm"
      }
    }
  }
}

 

2. Hierarchical Navigable Small Worlds(HNSW) 알고리즘

* 자세한 사항은 논문 Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs 을 읽는것을 추천한다.

 

Hierarchical Navigable Small Worlds (HNSW) 알고리즘은 Approximate k-Nearest Neighbor (ANN) 검색에서 가장 널리 사용되는 방법 중 하나이다. 엘라스틱서치의 k-NN 플러그인이 최초로 지원한 알고리즘이며, nmslib 유사도 검색 라이브러리에서도 매우 효율적으로 활용된다

이 알고리즘 핵심 아이디어는 서로 가까운 벡터를 연결하는 계층적 그래프를 구축하여 검색을 최적화하는 것이다.

HNSW의 작동 방식

HNSW는 연결 그래프(connected graph) 를 사용하여 벡터 간 관계를 표현한다.  쿼리가 주어지면, 알고리즘은 이 그래프를 탐색하며 가장 가까운 이웃을 찾는다. 탐색 과정에서 항상 현재 위치에서 가장 가까운 후보를 선택하여 이동하는 방식으로 최적의 검색 속도를 유지한다.

 

OpenSearch의 HNSW 알고리즘에 대해 세 가지 매개 변수를 조정할 수 있다.

  • m – 벡터가 그래프에서 얻을 수 있는 최대 엣지의 수. 이 값이 클수록 그래프에서 더 많은 메모리를 사용하지만 검색시 근사치가 더 좋아질 수 있다.
  • ef_search – 탐색 중에 방문할 후보 노드의 대기열 크기. 노드를 방문하면 해당 노드의 이웃 노드가 향후 방문할 대기열에 추가. 값이 클수록 검색 대기 시간은 증가하지만 검색시 근사치가 더 좋아질 수 있다.
  • ef_construction –  탐색을 위한 후보 대기열 크기를 제어. 값이 클수록 인덱스 대기 시간은 증가하지만 더 나은 검색시 근사치를 제공할 수 있다.
PUT my_index
{
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "dense_vector",
        "dims": 128,
        "index": true,
        "similarity": "l2_norm",
        "index_options": {
          "type": "hnsw",
          "m": 16,    // 그래프 간선 개수 조정
          "ef_construction": 200 // 인덱싱 시 검색 후보 개수
        }
      }
    }
  }
}

 

 

3. Inverted File System(IVF) 알고리즘

Inverted File System (IVF) 알고리즘은 벡터를 여러 버킷(bucket) 으로 나누어 검색 속도를 최적화하는 방법으로, 검색 시 전체 데이터셋을 탐색하는 대신, 특정 버킷만 조회하여 검색 시간을 단축할 수 있다.

es k-NN 플러그인은 Faiss의 IVF 구현을 지원하기 시작했다. [Faiss는 Meta에서 개발한 고밀도 벡터의 효율적인 유사도 검색 및 클러스터링을 위한 오픈소스 라이브러리]

IVF의 작동 방식

IVF의 핵심 아이디어는 벡터를 무작위로 나누는 것이 아니라, 유사한 벡터끼리 같은 버킷에 배치하는 것. 이를 위해 버킷마다 대표 벡터(centroid)를 설정하고, 새로운 벡터가 색인될 때 가장 가까운 대표 벡터를 가진 버킷에 저장되어 서로 유사한 벡터들이 동일하거나 인접한 버킷에 위치하게 된다.

PUT my_index
{
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "dense_vector",
        "dims": 128,
        "index": true,
        "similarity": "l2_norm",
        "index_options": {
          "type": "ivf",
          "nlist": 100  // IVF 리스트 개수 조정
        }
      }
    }
  }
}

 

 

이외에도 정말 여러가지 알고리즘과 구현체가 있다. 자신의 상황에 맞게 골라서 써야한다...

자세한 사항은 아래의 링크들을 참조하길

 

 

끝!

 

참고 사항

https://aws.amazon.com/ko/blogs/tech/choose-the-k-nn-algorithm-for-your-billion-scale-use-case-with-opensearch/

https://ivoryrabbit.github.io/posts/IVF/

https://www.elastic.co/search-labs/blog/introduction-to-vector-search

https://seo-jinbro.github.io/ann/IVF_PQ_Explained/

https://medium.com/@dk02315/opensearch-%ED%82%A4%EC%9B%8C%EB%93%9C-%EB%B2%A1%ED%84%B0-%EA%B8%B0%EB%B0%98-hybrid-search-%EA%B5%AC%ED%98%84-fe1426910b26