메모리 최적화 벡터

메모리 최적화 벡터 (Memory-optimized vectors)

벡터 검색 연산은 특히 대규모 배포에서 메모리를 많이 사용할 수 있어요. OpenSearch는 검색 성능을 유지하면서 메모리 사용을 최적화하는 여러 전략을 제공해요. 낮은 지연 시간을 우선시하거나 낮은 비용을 우선시하는 다양한 워크로드 모드를 선택할 수 있고, 메모리 사용을 줄이기 위해 다양한 압축 수준(compression level)을 적용하거나, byte 또는 binary 벡터 같은 대체 벡터 표현을 사용할 수 있어요. 이런 최적화 기법을 통해 특정 사용 사례 요구사항에 맞춰 메모리 사용량, 검색 성능, 비용 사이의 균형을 조절할 수 있어요.

출처: 문서

본문

벡터 워크로드 모드 (Vector workload modes)

벡터 검색은 검색 성능과 운영 비용 사이의 균형이 필요해요. 인메모리 검색은 가장 낮은 지연 시간을 제공하지만, 디스크 기반 검색은 메모리 사용을 줄여 더 비용 효율적인 접근 방식을 제공하고, 대신 검색 지연 시간이 약간 높아져요. 이 접근 방식 중 하나를 선택하려면 knn_vector 필드 구성에서 mode 매핑 파라미터를 사용해요. 이 파라미터는 낮은 지연 시간 또는 낮은 비용이라는 우선순위에 따라 k-NN 파라미터에 적절한 기본값을 설정해요. 추가 최적화를 위해 k-NN 필드 매핑에서 이 기본 파라미터 값을 재정의할 수 있어요.

OpenSearch는 다음 벡터 워크로드 모드를 지원해요.

모드 기본 엔진 설명
in_memory (기본값) faiss 낮은 지연 시간 검색을 우선시해요. 이 모드는 양자화 없이 faiss 엔진을 사용해요. OpenSearch의 벡터 검색 기본 파라미터 값으로 구성돼요.
on_disk faiss 강력한 재현율(recall)을 유지하면서 낮은 비용 벡터 검색을 우선시해요. 기본적으로 on_disk 모드는 양자화와 재스코어링(rescoring)을 사용해 2단계 방식으로 상위 이웃을 검색해요. on_disk 모드는 float 벡터 타입만 지원해요.

낮은 비용 검색을 위해 on_disk 모드를 사용하는 벡터 인덱스를 만들려면 다음 요청을 보내요.

PUT test-index
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "knn_vector",
        "dimension": 3,
        "space_type": "l2",
        "mode": "on_disk"
      }
    }
  }
}

압축 수준 (Compression levels)

compression_level 매핑 파라미터는 주어진 배수만큼 벡터 메모리 사용을 줄이는 양자화 인코더를 선택해요. 사용 가능한 compression_level 값은 다음 표와 같아요.

압축 수준 지원 엔진
1x faiss, lucene, nmslib (deprecated)
2x faiss
4x lucene
8x faiss
16x faiss
32x faiss와 lucene

예를 들어, 768차원 벡터의 float32 인덱스에 compression_level 32x를 전달하면 벡터당 메모리가 4 * 768 = 3072 바이트에서 3072 / 32 = 846 바이트로 줄어요. 내부적으로 이 압축을 달성하기 위해 이진 양자화(float를 비트로 매핑)가 사용될 수 있어요.

compression_level 파라미터를 설정하면 method 매핑에 encoder를 지정할 수 없어요. 1x보다 큰 압축 수준은 float 벡터 타입에만 지원돼요.

OpenSearch 3.1부터, on_disk 모드를 1x 압축 수준과 함께 활성화하면 메모리 최적화 검색(memory-optimized search)이 활성화돼요. 이 모드에서 엔진은 모든 데이터를 한 번에 메모리에 로드하는 대신, 검색 중에 데이터를 필요에 따라(on demand) 로드해요.

사용 가능한 워크로드 모드의 기본 compression_level 값은 다음 표와 같아요.

모드 기본 압축 수준
in_memory 1x
on_disk 32x

compression_level이 16x인 벡터 필드를 만들려면 매핑에 compression_level 파라미터를 지정해요. 이 파라미터는 on_disk 모드의 기본 압축 수준을 32x에서 16x로 재정의하며, 메모리 사용량이 커지는 대신 더 높은 재현율과 정확도를 만들어요.

PUT test-index
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "knn_vector",
        "dimension": 3,
        "space_type": "l2",
        "mode": "on_disk",
        "compression_level": "16x"
      }
    }
  }
}

양자화 결과를 전체 정밀도로 재스코어링

양자화의 메모리 절약을 유지하면서 재현율을 개선하려면 2단계 검색 접근 방식을 사용할 수 있어요. 첫 번째 단계에서 양자화된 벡터를 사용해 인덱스에서 oversample_factor * k 개의 결과를 검색하고 점수를 근사해요. 두 번째 단계에서는 그 oversample_factor * k 개 결과의 전체 정밀도 벡터를 디스크에서 메모리로 로드하고, 전체 정밀도 쿼리 벡터에 대해 점수를 다시 계산해요. 그런 다음 결과를 상위 k개로 축소해요.

기본 재스코어링 동작은 지원 k-NN 벡터 필드의 mode와 compression_level에 의해 결정돼요.

  • in_memory 모드에서는 기본적으로 재스코어링이 적용되지 않아요.
  • on_disk 모드에서는 기본 재스코어링이 구성된 compression_level에 기반해요. 각 압축 수준은 다음 표에 지정된 기본 oversample_factor를 제공해요.
압축 수준 기본 재스코어 oversample_factor
32x (기본값) 2.0
16x 3.0
8x 2.0
4x 1.0
2x 기본 재스코어링 없음

명시적으로 재스코어링을 적용하려면 양자화된 인덱스의 쿼리에서 rescore 파라미터를 제공하고 oversample_factor를 지정해요.

GET /my-vector-index/_search
{
  "size": 2,
  "query": {
    "knn": {
      "target-field": {
        "vector": [2, 3, 5, 6],
        "k": 2,
        "rescore" : {
          "oversample_factor": 1.2
        }
      }
    }
  }
}

또는 rescore 파라미터를 true로 설정해 기본값인 1.0의 oversample_factor를 사용할 수 있어요.

GET /my-vector-index/_search
{
  "size": 2,
  "query": {
    "knn": {
      "target-field": {
        "vector": [2, 3, 5, 6],
        "k": 2,
        "rescore" : true
      }
    }
  }
}

oversample_factor는 1.0 이상 100.0 이하(포함)의 부동소수점 숫자예요. 첫 번째 패스의 결과 수는 oversample_factor * k로 계산되며, 100 이상 10,000 이하(포함)가 보장돼요. 계산된 결과 수가 100보다 작으면 결과 수는 100으로 설정되고, 10,000보다 크면 결과 수는 10,000으로 설정돼요.

  • 재스코어링은 Faiss와 Lucene 엔진에서만 사용할 수 있어요.
  • 양자화를 사용하지 않으면 반환되는 점수가 이미 완전히 정밀하기 때문에 재스코어링이 필요 없어요.

Byte 벡터

기본적으로 k-NN 벡터는 각 차원이 4바이트인 float 벡터예요. 저장 공간을 절약하고 싶다면 faiss 또는 lucene 엔진에서 byte 벡터를 사용할 수 있어요. byte 벡터에서 각 차원은 [-128, 127] 범위의 부호 있는 8비트 정수예요.

  • byte 벡터는 lucene과 faiss 엔진에서만 지원돼요. nmslib 엔진에서는 지원되지 않아요.
  • k-NN 벤치마킹 테스트에서 float 벡터 대신 byte 벡터를 사용했을 때 저장 공간과 메모리 사용이 크게 줄었고, 색인 처리량이 향상되고 쿼리 지연 시간이 감소했어요. 또한 재현율 정밀도는 크게 영향받지 않았어요(재현율은 사용된 양자화 기법과 데이터 분포 같은 다양한 요인에 따라 달라질 수 있다는 점에 유의하세요).
  • byte 벡터를 사용할 때는 float 벡터에 비해 일부 재현율 정밀도 손실이 있을 것으로 예상해야 해요. byte 벡터는 최소한의 재현율 손실을 대가로 메모리 사용량을 줄이는 것을 우선시하는 대규모 애플리케이션과 사용 사례에 유용해요.
  • faiss 엔진에서 byte 벡터를 사용할 때는 SIMD(Single Instruction Multiple Data) 최적화를 사용하는 것을 권장해요. 이 최적화는 검색 지연 시간을 크게 줄이고 색인 처리량을 개선하는 데 도움이 돼요.

k-NN 플러그인 버전 2.9에서 도입된 선택적 data_type 파라미터는 벡터의 데이터 타입을 정의해요. 이 파라미터의 기본값은 float예요.

byte 벡터를 사용하려면 인덱스의 매핑을 만들 때 data_type 파라미터를 byte로 설정해요.

예시: HNSW

다음 예시는 lucene 엔진과 hnsw 알고리즘으로 byte 벡터 인덱스를 만들어요.

PUT test-index
{
  "settings": {
    "index": {
      "knn": true,
      "knn.algo_param.ef_search": 100
    }
  },
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "knn_vector",
        "dimension": 3,
        "data_type": "byte",
        "space_type": "l2",
        "method": {
          "name": "hnsw",
          "engine": "lucene",
          "parameters": {
            "ef_construction": 100,
            "m": 16
          }
        }
      }
    }
  }
}

인덱스를 만든 뒤에는 평소처럼 문서를 수집해요. 벡터의 각 차원이 지원 범위 [-128, 127] 안에 있는지 확인하세요.

PUT test-index/_doc/1
{
  "my_vector": [-126, 28, 127]
}
PUT test-index/_doc/2
{
  "my_vector": [100, -128, 0]
}

쿼리할 때는 반드시 byte 벡터를 사용하세요.

GET test-index/_search
{
  "size": 2,
  "query": {
    "knn": {
      "my_vector": {
        "vector": [26, -120, 99],
        "k": 2
      }
    }
  }
}

예시: IVF

ivf 메서드는 세그먼트 생성 중에 네이티브 라이브러리 인덱스를 초기화하도록 모델을 만들고 훈련하는 훈련 단계가 필요해요. 자세한 내용은 모델에서 벡터 인덱스 만들기를 참고하세요.

먼저 byte 벡터 훈련 데이터를 담을 인덱스를 만들어요. faiss 엔진과 ivf 알고리즘을 지정하고, 차원이 만들고자 하는 모델의 차원과 일치하는지 확인하세요.

PUT train-index
{
  "mappings": {
    "properties": {
      "train-field": {
        "type": "knn_vector",
        "dimension": 4,
        "data_type": "byte"
      }
    }
  }
}

먼저 byte 벡터를 포함한 훈련 데이터를 훈련 인덱스에 수집해요.

PUT _bulk
{ "index": { "_index": "train-index", "_id": "1" } }
{ "train-field": [127, 100, 0, -120] }
{ "index": { "_index": "train-index", "_id": "2" } }
{ "train-field": [2, -128, -10, 50] }
{ "index": { "_index": "train-index", "_id": "3" } }
{ "train-field": [13, -100, 5, 126] }
{ "index": { "_index": "train-index", "_id": "4" } }
{ "train-field": [5, 100, -6, -125] }

그런 다음 byte-vector-model이라는 이름의 모델을 만들고 훈련해요. 모델은 train-index의 train-field에 있는 훈련 데이터로 훈련돼요. byte 데이터 타입을 지정해요.

POST _plugins/_knn/models/byte-vector-model/_train
{
  "training_index": "train-index",
  "training_field": "train-field",
  "dimension": 4,
  "description": "model with byte data",
  "data_type": "byte",
  "method": {
    "name": "ivf",
    "engine": "faiss",
    "space_type": "l2",
    "parameters": {
      "nlist": 1,
      "nprobes": 1
    }
  }
}

모델 훈련 상태를 확인하려면 Get Model API를 호출해요.

GET _plugins/_knn/models/byte-vector-model?filter_path=state

훈련이 완료되면 상태가 created로 바뀌어요.

다음으로, 훈련된 모델을 사용해 네이티브 라이브러리 인덱스를 초기화할 인덱스를 만들어요.

PUT test-byte-ivf
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "knn_vector",
        "model_id": "byte-vector-model"
      }
    }
  }
}

검색하고 싶은 byte 벡터가 포함된 데이터를 만든 인덱스에 수집해요.

PUT _bulk?refresh=true
{"index": {"_index": "test-byte-ivf", "_id": "1"}}
{"my_vector": [7, 10, 15, -120]}
{"index": {"_index": "test-byte-ivf", "_id": "2"}}
{"my_vector": [10, -100, 120, -108]}
{"index": {"_index": "test-byte-ivf", "_id": "3"}}
{"my_vector": [1, -2, 5, -50]}
{"index": {"_index": "test-byte-ivf", "_id": "4"}}
{"my_vector": [9, -7, 45, -78]}
{"index": {"_index": "test-byte-ivf", "_id": "5"}}
{"my_vector": [80, -70, 127, -128]}

마지막으로 데이터를 검색해요. k-NN 벡터 필드에 반드시 byte 벡터를 제공하세요.

GET test-byte-ivf/_search
{
  "size": 2,
  "query": {
    "knn": {
      "my_vector": {
        "vector": [100, -120, 50, -45],
        "k": 2
      }
    }
  }
}

메모리 추정 (Memory estimation)

최상의 시나리오에서 byte 벡터는 32비트 벡터가 필요로 하는 메모리의 25%만 필요해요.

HNSW 메모리 추정

HNSW(Hierarchical Navigable Small World)에 필요한 메모리는 1.1 * (dimension + 8 * m) bytes/vector로 추정되며, 여기서 m은 그래프 구성 중 각 요소에 대해 생성되는 양방향 링크의 최대 수예요.

예를 들어 차원이 256이고 m이 16인 벡터 100만 개가 있다고 가정해요. 메모리 요구량은 다음과 같이 추정할 수 있어요.

1.1 * (256 + 8 * 16) * 1,000,000 ~= 0.39 GB

IVF 메모리 추정

IVF(Inverted File Index)에 필요한 메모리는 1.1 * ((dimension * num_vectors) + (4 * nlist * dimension)) bytes/vector로 추정되며, 여기서 nlist는 벡터를 분할할 버킷 수예요.

예를 들어 차원이 256이고 nlist가 128인 벡터 100만 개가 있다고 가정해요. 메모리 요구량은 다음과 같이 추정할 수 있어요.

1.1 * ((256 * 1,000,000) + (4 * 128 * 256))  ~= 0.27 GB

양자화 기법 (Quantization techniques)

벡터가 float 타입이라면 문서를 수집하기 전에 먼저 byte 타입으로 변환해야 해요. 이 변환은 데이터셋을 양자화(벡터의 정밀도를 낮추는 것)해서 수행돼요. Faiss 엔진은 스칼라 양자화(SQ)와 곱 양자화(PQ) 같은 여러 양자화 기법을 지원해요. 양자화 기법의 선택은 사용 중인 데이터의 타입에 따라 달라지며 재현율 값의 정확도에 영향을 줄 수 있어요. 다음 섹션들은 L2와 cosine 유사도 space type에 대해 k-NN 벤치마킹 테스트 데이터를 양자화하는 데 사용된 스칼라 양자화 알고리즘을 설명해요. 제공되는 의사코드는 설명 목적을 위한 것일 뿐이에요.

L2 space type에 대한 스칼라 양자화

다음 예시 의사코드는 L2 space type을 사용하는 유클리드 데이터셋에 대한 벤치마킹 테스트에 사용된 스칼라 양자화 기법을 설명해요. 유클리드 거리는 이동 불변(shift invariant)이에요. x와 y를 같은 z만큼 이동하면 거리는 그대로 유지돼요(||x-y|| = ||(x-z)-(y-z)||).

# Random dataset (Example to create a random dataset)
dataset = np.random.uniform(-300, 300, (100, 10))
# Random query set (Example to create a random queryset)
queryset = np.random.uniform(-350, 350, (100, 10))
# Number of values
B = 256

# INDEXING:
# Get min and max
dataset_min = np.min(dataset)
dataset_max = np.max(dataset)
# Shift coordinates to be non-negative
dataset -= dataset_min
# Normalize into [0, 1]
dataset *= 1. / (dataset_max - dataset_min)
# Bucket into 256 values
dataset = np.floor(dataset * (B - 1)) - int(B / 2)

# QUERYING:
# Clip (if queryset range is out of datset range)
queryset = queryset.clip(dataset_min, dataset_max)
# Shift coordinates to be non-negative
queryset -= dataset_min
# Normalize
queryset *= 1. / (dataset_max - dataset_min)
# Bucket into 256 values
queryset = np.floor(queryset * (B - 1)) - int(B / 2)

cosine 유사도 space type에 대한 스칼라 양자화

다음 예시 의사코드는 cosine 유사도 space type을 사용하는 각도 데이터셋에 대한 벤치마킹 테스트에 사용된 스칼라 양자화 기법을 설명해요. cosine 유사도는 이동 불변이 아니에요(cos(x, y) ≠ cos(x-z, y-z)).

다음 의사코드는 양수용이에요.

# For Positive Numbers

# INDEXING and QUERYING:

# Get Max of train dataset
max = np.max(dataset)
min = 0
B = 127

# Normalize into [0,1]
val = (val - min) / (max - min)
val = (val * B)

# Get int and fraction values
int_part = floor(val)
frac_part = val - int_part

if 0.5 < frac_part:
    val = int_part + 1
else:
    val = int_part

다음 의사코드는 음수용이에요.

# For Negative Numbers

# INDEXING and QUERYING:

# Get Min of train dataset
min = 0
max = -np.min(dataset)
B = 128

# Normalize into [0,1]
val = (val - min) / (max - min)
val = (val * B)

# Get int and fraction values
int_part = floor(var)
frac_part = val - int_part

if 0.5 < frac_part:
    val = int_part + 1
else:
    val = int_part

Binary 벡터

float에서 binary 벡터로 전환하면 메모리 비용을 32배로 줄일 수 있어요. binary 벡터 인덱스를 사용하면 높은 재현율 성능을 유지하면서 운영 비용을 낮출 수 있어, 대규모 배포를 더 경제적이고 효율적으로 만들어요.

binary 형식은 다음 k-NN 검색 타입에서 사용할 수 있어요.

  • 근사 k-NN (Approximate k-NN): HNSW와 IVF 알고리즘으로 Faiss 엔진에서만 binary 벡터를 지원해요.
  • 스크립트 스코어 k-NN (Script score k-NN): 스크립트 스코어링에서 binary 벡터를 사용할 수 있게 해요.
  • Painless 확장 (Painless extensions): Painless 스크립팅 확장에서 binary 벡터를 사용할 수 있게 해요.

요구사항 (Requirements)

OpenSearch k-NN 플러그인에서 binary 벡터를 사용하기 위한 여러 요구사항이 있어요.

  • binary 벡터 인덱스의 data_type은 binary여야 해요.
  • binary 벡터 인덱스의 space_type은 hamming이어야 해요.
  • binary 벡터 인덱스의 차원(dimension)은 8의 배수여야 해요.
  • binary 데이터를 [-128, 127] 범위의 8비트 부호 있는 정수(int8)로 변환해야 해요. 예를 들어, 8비트 0, 1, 1, 0, 0, 0, 1, 1의 이진 시퀀스는 binary 벡터 입력으로 사용되려면 그에 해당하는 99라는 byte 값으로 변환되어야 해요.

예시: HNSW

Faiss 엔진과 HNSW 알고리즘으로 binary 벡터 인덱스를 만들려면 다음 요청을 보내요.

PUT /test-binary-hnsw
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "knn_vector",
        "dimension": 8,
        "data_type": "binary",
        "space_type": "hamming",
        "method": {
          "name": "hnsw",
          "engine": "faiss"
        }
      }
    }
  }
}

그런 다음 binary 벡터가 포함된 몇 가지 문서를 수집해요.

PUT _bulk
{"index": {"_index": "test-binary-hnsw", "_id": "1"}}
{"my_vector": [7], "price": 4.4}
{"index": {"_index": "test-binary-hnsw", "_id": "2"}}
{"my_vector": [10], "price": 14.2}
{"index": {"_index": "test-binary-hnsw", "_id": "3"}}
{"my_vector": [15], "price": 19.1}
{"index": {"_index": "test-binary-hnsw", "_id": "4"}}
{"my_vector": [99], "price": 1.2}
{"index": {"_index": "test-binary-hnsw", "_id": "5"}}
{"my_vector": [80], "price": 16.5}

쿼리할 때는 반드시 binary 벡터를 사용하세요.

GET /test-binary-hnsw/_search
{
  "size": 2,
  "query": {
    "knn": {
      "my_vector": {
        "vector": [9],
        "k": 2
      }
    }
  }
}

응답에는 쿼리 벡터에 가장 가까운 두 벡터가 포함돼요.

{
  "took": 8,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.5,
    "hits": [
      {
        "_index": "test-binary-hnsw",
        "_id": "2",
        "_score": 0.5,
        "_source": {
          "my_vector": [
            10
          ],
          "price": 14.2
        }
      },
      {
        "_index": "test-binary-hnsw",
        "_id": "5",
        "_score": 0.25,
        "_source": {
          "my_vector": [
            80
          ],
          "price": 16.5
        }
      }
    ]
  }
}

예시: IVF

IVF 메서드는 세그먼트 생성 중에 네이티브 라이브러리 인덱스를 초기화하도록 모델을 만들고 훈련하는 훈련 단계가 필요해요. 자세한 내용은 모델에서 벡터 인덱스 만들기를 참고하세요.

먼저 binary 벡터 훈련 데이터를 담을 인덱스를 만들어요. Faiss 엔진과 IVF 알고리즘을 지정하고, 차원이 만들고자 하는 모델의 차원과 일치하는지 확인하세요.

PUT train-index
{
  "mappings": {
    "properties": {
      "train-field": {
        "type": "knn_vector",
        "dimension": 8,
        "data_type": "binary"
      }
    }
  }
}

binary 벡터가 포함된 훈련 데이터를 훈련 인덱스에 수집해요.

PUT _bulk
{ "index": { "_index": "train-index", "_id": "1" } }
{ "train-field": [1] }
{ "index": { "_index": "train-index", "_id": "2" } }
{ "train-field": [2] }
{ "index": { "_index": "train-index", "_id": "3" } }
{ "train-field": [3] }
{ "index": { "_index": "train-index", "_id": "4" } }
{ "train-field": [4] }
{ "index": { "_index": "train-index", "_id": "5" } }
{ "train-field": [5] }
{ "index": { "_index": "train-index", "_id": "6" } }
{ "train-field": [6] }
{ "index": { "_index": "train-index", "_id": "7" } }
{ "train-field": [7] }
{ "index": { "_index": "train-index", "_id": "8" } }
{ "train-field": [8] }
{ "index": { "_index": "train-index", "_id": "9" } }
{ "train-field": [9] }
{ "index": { "_index": "train-index", "_id": "10" } }
{ "train-field": [10] }
{ "index": { "_index": "train-index", "_id": "11" } }
{ "train-field": [11] }
{ "index": { "_index": "train-index", "_id": "12" } }
{ "train-field": [12] }
{ "index": { "_index": "train-index", "_id": "13" } }
{ "train-field": [13] }
{ "index": { "_index": "train-index", "_id": "14" } }
{ "train-field": [14] }
{ "index": { "_index": "train-index", "_id": "15" } }
{ "train-field": [15] }
{ "index": { "_index": "train-index", "_id": "16" } }
{ "train-field": [16] }
{ "index": { "_index": "train-index", "_id": "17" } }
{ "train-field": [17] }
{ "index": { "_index": "train-index", "_id": "18" } }
{ "train-field": [18] }
{ "index": { "_index": "train-index", "_id": "19" } }
{ "train-field": [19] }
{ "index": { "_index": "train-index", "_id": "20" } }
{ "train-field": [20] }
{ "index": { "_index": "train-index", "_id": "21" } }
{ "train-field": [21] }
{ "index": { "_index": "train-index", "_id": "22" } }
{ "train-field": [22] }
{ "index": { "_index": "train-index", "_id": "23" } }
{ "train-field": [23] }
{ "index": { "_index": "train-index", "_id": "24" } }
{ "train-field": [24] }
{ "index": { "_index": "train-index", "_id": "25" } }
{ "train-field": [25] }
{ "index": { "_index": "train-index", "_id": "26" } }
{ "train-field": [26] }
{ "index": { "_index": "train-index", "_id": "27" } }
{ "train-field": [27] }
{ "index": { "_index": "train-index", "_id": "28" } }
{ "train-field": [28] }
{ "index": { "_index": "train-index", "_id": "29" } }
{ "train-field": [29] }
{ "index": { "_index": "train-index", "_id": "30" } }
{ "train-field": [30] }
{ "index": { "_index": "train-index", "_id": "31" } }
{ "train-field": [31] }
{ "index": { "_index": "train-index", "_id": "32" } }
{ "train-field": [32] }
{ "index": { "_index": "train-index", "_id": "33" } }
{ "train-field": [33] }
{ "index": { "_index": "train-index", "_id": "34" } }
{ "train-field": [34] }
{ "index": { "_index": "train-index", "_id": "35" } }
{ "train-field": [35] }
{ "index": { "_index": "train-index", "_id": "36" } }
{ "train-field": [36] }
{ "index": { "_index": "train-index", "_id": "37" } }
{ "train-field": [37] }
{ "index": { "_index": "train-index", "_id": "38" } }
{ "train-field": [38] }
{ "index": { "_index": "train-index", "_id": "39" } }
{ "train-field": [39] }
{ "index": { "_index": "train-index", "_id": "40" } }
{ "train-field": [40] }

그런 다음 test-binary-model이라는 이름의 모델을 만들고 훈련해요. 모델은 train-index의 train_field에 있는 훈련 데이터로 훈련돼요. binary 데이터 타입과 hamming space type을 지정해요.

POST _plugins/_knn/models/test-binary-model/_train
{
  "training_index": "train-index",
  "training_field": "train-field",
  "dimension": 8,
  "description": "model with binary data",
  "data_type": "binary",
  "space_type": "hamming",
  "method": {
    "name": "ivf",
    "engine": "faiss",
    "parameters": {
      "nlist": 16,
      "nprobes": 1
    }
  }
}

모델 훈련 상태를 확인하려면 Get Model API를 호출해요.

GET _plugins/_knn/models/test-binary-model?filter_path=state

훈련이 완료되면 상태가 created로 바뀌어요.

다음으로, 훈련된 모델을 사용해 네이티브 라이브러리 인덱스를 초기화할 인덱스를 만들어요.

PUT test-binary-ivf
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "knn_vector",
        "model_id": "test-binary-model"
      }
    }
  }
}

검색하고 싶은 binary 벡터가 포함된 데이터를 만든 인덱스에 수집해요.

PUT _bulk?refresh=true
{"index": {"_index": "test-binary-ivf", "_id": "1"}}
{"my_vector": [7], "price": 4.4}
{"index": {"_index": "test-binary-ivf", "_id": "2"}}
{"my_vector": [10], "price": 14.2}
{"index": {"_index": "test-binary-ivf", "_id": "3"}}
{"my_vector": [15], "price": 19.1}
{"index": {"_index": "test-binary-ivf", "_id": "4"}}
{"my_vector": [99], "price": 1.2}
{"index": {"_index": "test-binary-ivf", "_id": "5"}}
{"my_vector": [80], "price": 16.5}

마지막으로 데이터를 검색해요. k-NN 벡터 필드에 반드시 binary 벡터를 제공하세요.

GET test-binary-ivf/_search
{
  "size": 2,
  "query": {
    "knn": {
      "my_vector": {
        "vector": [8],
        "k": 2
      }
    }
  }
}

응답에는 쿼리 벡터에 가장 가까운 두 벡터가 포함돼요.

GET /_plugins/_knn/models/my-model?filter_path=state
{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.5,
    "hits": [
      {
        "_index": "test-binary-ivf",
        "_id": "2",
        "_score": 0.5,
        "_source": {
          "my_vector": [
            10
          ],
          "price": 14.2
        }
      },
      {
        "_index": "test-binary-ivf",
        "_id": "3",
        "_score": 0.25,
        "_source": {
          "my_vector": [
            15
          ],
          "price": 19.1
        }
      }
    ]
  }
}

메모리 추정 (Memory estimation)

다음 공식을 사용해 binary 벡터에 필요한 메모리 양을 추정해요.

HNSW 메모리 추정

HNSW에 필요한 메모리는 다음 공식으로 추정할 수 있어요. 여기서 m은 그래프 구성 중 각 요소에 대해 생성되는 양방향 링크의 최대 수예요.

1.1 * (dimension / 8 + 8 * m) bytes/vector

IVF 메모리 추정

IVF에 필요한 메모리는 다음 공식으로 추정할 수 있어요. 여기서 nlist는 벡터를 분할할 버킷 수예요.

1.1 * (((dimension / 8) * num_vectors) + (nlist * dimension / 8))

다음 단계 (Next steps)

  • k-NN 쿼리
  • 디스크 기반 벡터 검색
  • 벡터 양자화

더 알아보기 (Learn more)