블로그

더 나은 바이너리 정량화(BBQ)를 사용 사례에 구현하는 방법

사용 사례에서 더 나은 이진 정량화(BBQ)를 구현하는 이유와 그 방법을 살펴보세요.

벡터 검색은 텍스트에 대한 시맨틱 검색이나 이미지, 동영상 또는 오디오에 대한 유사도 검색을 구현할 때 기초를 제공합니다. 벡터 검색에서 벡터는 방대하고 때로는 느릴 수 있는 데이터를 수학적으로 표현한 것입니다. 더 나은 이진 양자화(이하 BBQ)는 벡터의 압축 방법으로 작동합니다. 벡터를 축소하여 검색 및 처리 속도를 높이면서 적합한 일치 항목을 찾을 수 있습니다. 이 글에서는 벡터를 자동으로 채점하는 정량화된 인덱스에만 사용할 수 있는 필드인 BBQ와 rescore_vector에 대해 설명합니다.

이 문서에 언급된 모든 전체 쿼리와 출력은 Elasticsearch Labs 코드 리포지토리에서 확인할 수 있습니다.

사용 사례에서 더 나은 이진 정량화(BBQ)를 구현하는 이유는 무엇인가요?

참고: BBQ의 수학적 원리에 대한 자세한 내용은 아래의 '추가 학습' 섹션을 참조하세요. 이 블로그에서는 구현에 초점을 맞추고 있습니다.

수학은 흥미롭지만, 벡터 검색의 정확성을 유지하려면 그 이유를 완전히 파악하는 것이 중요합니다. 현재의 벡터 검색 알고리즘으로는 데이터 읽기 속도에 제한이 있다는 것이 밝혀졌기 때문에 궁극적으로 이것은 압축에 관한 것입니다. 따라서 해당 데이터를 모두 메모리에 넣을 수 있다면 스토리지에서 읽을 때보다 속도가 크게 향상됩니다(메모리는 SSD보다 약 200배 빠릅니다).

몇 가지 유의해야 할 사항이 있습니다:

  • HNSW (계층 탐색 가능한 작은 세계)와 같은 그래프 기반 인덱스는 벡터 검색에 가장 빠릅니다.

    • HNSW: 다층 그래프 구조를 구축하여 효율적인 고차원 유사도 검색을 가능하게 하는 근사 근사 이웃 검색 알고리즘입니다.

HNSW: 다층 그래프 구조를 구축하여 효율적인 고차원 유사도 검색을 가능하게 하는 근사 근사 이웃 검색 알고리즘입니다.
  • HNSW는 메모리 또는 최악의 경우 스토리지에서 데이터를 읽는 속도에 의해 근본적으로 속도가 제한됩니다.

    • 이상적으로는 저장된 모든 벡터를 메모리에 로드할 수 있어야 합니다.

  • 임베딩 모델은 일반적으로 부동 소수점 숫자당 4바이트의 플로트32 정밀도를 가진 벡터를 생성합니다.

  • 마지막으로, 보유하고 있는 벡터 및/또는 치수의 수에 따라 모든 벡터를 저장하기 위한 메모리가 매우 빠르게 부족해질 수 있습니다.

이를 당연하게 생각하면, 수백 또는 수천 개의 차원을 가진 수백만 또는 수십억 개의 벡터를 수집하기 시작하면 문제가 빠르게 발생한다는 것을 알 수 있습니다. '압축률에 대한 대략적인수치'라는 제목의 섹션에서 대략적인 수치를 확인할 수 있습니다.

시작하려면 무엇이 필요하나요?

시작하려면 다음이 필요합니다:

  • Elastic Cloud 또는 온프레미스를 사용하는 경우, 8.18 이상의 Elasticsearch 버전이 필요합니다. BBQ는 8.16에 도입되었지만, 이 글에서는 8.18에 도입된 vector_rescore 을 사용합니다.

  • 또한 클러스터에 머신 러닝(ML) 노드가 있는지 확인해야 합니다. (참고: 모델을 로드하려면 최소 4GB의 ML 노드가 필요하지만 전체 프로덕션 워크로드에는 훨씬 더 큰 노드가 필요할 수 있습니다.)

  • 서버리스를 사용하는 경우 벡터에 최적화된 인스턴스를 선택해야 합니다.

  • 또한 벡터 데이터베이스에 대한 기본 지식이 필요합니다. Elastic의 벡터 검색 개념에 아직 익숙하지 않으시다면 먼저 다음 리소스를 확인해보시는 것이 좋습니다:

더 나은 바이너리 정량화(BBQ) 구현

Elasticsearch bbq 구현.

이 블로그는 간결하게 유지하기 위해 기본 제공 기능을 사용할 수 있는 경우 이를 사용합니다. 이 경우, 머신 러닝 노드에서 Elasticsearch 내부에서 직접 실행되는 .multilingual-e5-small 벡터 임베딩 모델이 있습니다. text_embedding 모델을 원하는 임베더(OpenAI, Google AI Studio, Cohere 등)로 대체할 수 있습니다. 선호하는 모델이 아직 통합되지 않은 경우, 자체 고밀도 벡터 임베딩을 가져올 수도 있습니다.)

먼저, 주어진 텍스트에 대한 벡터를 생성하기 위해 추론 엔드포인트를 만들어야 합니다. 이 모든 명령은 Kibana 개발자 도구 콘솔에서 실행합니다. 이 명령은 .multilingual-e5-small 을 다운로드합니다. 아직 존재하지 않으면 엔드포인트가 설정되며, 실행하는 데 1분 정도 걸릴 수 있습니다. 예상 출력은 출력 폴더의 01-create-an-inference-endpoint-output.json 파일에서 확인할 수 있습니다.

PUT _inference/text_embedding/my_e5_model
{
  "service": "elasticsearch",
  "service_settings": {
    "num_threads": 1,
    "model_id": ".multilingual-e5-small",
    "adaptive_allocations": {
      "enabled": true,
      "min_number_of_allocations": 1
    }
  }
}

반환되면 모델이 설정되고 다음 명령을 사용하여 모델이 예상대로 작동하는지 테스트할 수 있습니다. 예상 출력은 출력 폴더의 02-embed-text-output.json 파일에서 확인할 수 있습니다.

POST _inference/text_embedding/my_e5_model
{
  "input": "my awesome piece of text"
}

학습된 모델이 노드에 할당되지 않는 문제가 발생하면 모델을 수동으로 시작해야 할 수 있습니다.

POST _ml/trained_models/.multilingual-e5-small/deployment/_start

이제 임베딩 모델의 출력과 일치하도록 표준 텍스트 필드(my_field)와 384개의 차원을 가진 고밀도 벡터 필드(my_vector)의 두 가지 속성을 가진 새 매핑을 만들어 보겠습니다. 또한 index_options.type to bbq_hnsw 을 재정의합니다. 예상 출력은 출력 폴더의 03-create-byte-qauntized-index-output.json 파일에서 확인할 수 있습니다.

PUT bbq-my-byte-quantized-index
{
  "mappings": {
    "properties": {
      "my_field": {
        "type": "text"
      },
      "my_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index_options": {
          "type": "bbq_hnsw"
        }
      }
    }
  }
}

Elasticsearch가 벡터를 생성하도록 하기 위해 수집 파이프라인을 사용할 수 있습니다. 이 파이프라인에는 엔드포인트(model_id), 벡터를 생성하려는 input_field, 벡터를 저장할 output_field, 이 세 가지가 필요합니다. 아래의 첫 번째 명령은 추론 서비스를 내부적으로 사용하는 추론 수집 파이프라인을 만들고, 두 번째 명령은 파이프라인이 올바르게 작동하는지 테스트합니다. 예상 출력은 출력 폴더의 04-create-and-simulate-ingest-pipeline-output.json 파일에서 확인할 수 있습니다. 

PUT _ingest/pipeline/my_inference_pipeline
{
  "processors": [
    {
      "inference": {
        "model_id": "my_e5_model",
        "input_output": [
          {
            "input_field": "my_field",
            "output_field": "my_vector"
          }
        ]
      }
    }
  ]
}

POST _ingest/pipeline/my_inference_pipeline/_simulate
{
  "docs": [
    {
      "_source": {
        "my_field": "my awesome text field"
      }
    }
  ]
}

이제 아래의 처음 두 명령어로 문서를 추가하고 세 번째 명령어로 검색이 제대로 작동하는지 테스트할 준비가 되었습니다. 예상 출력은 출력 폴더의 05-bbq-index-output.json 파일에서 확인할 수 있습니다. 

PUT bbq-my-byte-quantized-index/_doc/1?pipeline=my_inference_pipeline
{
    "my_field": "my awesome text field"
}

PUT bbq-my-byte-quantized-index/_doc/2?pipeline=my_inference_pipeline
{
    "my_field": "some other sentence"
}

GET bbq-my-byte-quantized-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}

이 게시물에서 권장하는 바와 같이, 리스코어링과 오버샘플링은 압축의 이점을 활용하면서 높은 리콜 정확도를 유지하는 데 도움이 되므로 데이터 양이 많지 않은 경우 확장하는 것이 좋습니다. Elasticsearch 버전 8.18부터는 rescore_vector를 사용하여 이 작업을 수행할 수 있습니다. 예상 출력은 출력 폴더의 06-bbq-search-8-18-output.json 파일에 있습니다.

GET bbq-my-byte-quantized-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "rescore_vector": {
              "oversample": 3
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}

이 점수는 원시 데이터에 대해 얻을 수 있는 점수와 어떻게 비교될까요? 위의 모든 작업을 다시 수행하되 index_options.type: hnsw 을 사용하면 점수가 매우 비슷하다는 것을 알 수 있습니다. 예상 출력은 출력 폴더의 07-raw-vector-output.json 파일에서 확인할 수 있습니다.

PUT my-raw-vector-index
{
  "mappings": {
    "properties": {
      "my_field": {
        "type": "text"
      },
      "my_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index_options": {
          "type": "hnsw"
        }
      }
    }
  }
}

PUT my-raw-vector-index/_doc/1?pipeline=my_inference_pipeline
{
    "my_field": "my awesome text field"
}

PUT my-raw-vector-index/_doc/2?pipeline=my_inference_pipeline
{
    "my_field": "some other sentence"
}

GET my-raw-vector-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}

압축률에 대한 대략적인 수치

벡터 검색으로 작업할 때 저장 공간과 메모리 요구 사항은 금방 큰 문제가 될 수 있습니다. 다음 분석은 다양한 양자화 기법이 벡터 데이터의 메모리 사용량을 어떻게 획기적으로 줄이는지 보여줍니다.

벡터(V)

치수(D)

원시(V x D x 4)

int8 (V x (D x 1 + 4))

int4 (V x (D x 0.5 + 4))

bbq (V x (D x 0.125 + 4))

10,000,000

384

14.31GB

3.61GB

1.83GB

0.58GB

50,000,000

384

71.53GB

18.07GB

9.13GB

2.89GB

100,000,000

384

143.05GB

36.14GB

18.25GB

5.77GB

결론

BBQ는 정확도 저하 없이 압축을 위해 벡터 데이터에 적용할 수 있는 최적화 기능입니다. 벡터를 비트로 변환하여 데이터를 효과적으로 검색하고 AI 워크플로우를 확장하여 검색을 가속화하고 데이터 저장 공간을 최적화할 수 있도록 지원합니다.

추가 학습

BBQ에 대해 더 자세히 알고 싶다면 다음 리소스를 확인하세요:

관련 콘텐츠

LINQ to Elasticsearch ES|QL: C# 작성, Elasticsearch 쿼리

Florian Bernd

Elasticsearch에서 NLP와 벡터 검색으로 챗봇 기능 강화하기

Priscilla Parodi

Elasticsearch와 OpenSearch: 벡터 검색 성능 비교

Ugo Sangiorgi

AI 표절: Elasticsearch를 통한 표절 탐지

Priscilla Parodi

고급 RAG 기술 2부: 쿼리 및 테스트

Han Xiang Choong

최첨단 검색 환경을 구축할 준비가 되셨나요?

충분히 고급화된 검색은 한 사람의 노력만으로는 달성할 수 없습니다. Elasticsearch는 여러분과 마찬가지로 검색에 대한 열정을 가진 데이터 과학자, ML 운영팀, 엔지니어 등 많은 사람들이 지원합니다. 서로 연결하고 협력하여 원하는 결과를 얻을 수 있는 마법 같은 검색 환경을 구축해 보세요.

직접 사용해 보세요