kNN 검색

원본 보기

Elasticsearch의 kNN 검색

k-최근접 이웃(kNN) 검색은 코사인이나 L2 norm 같은 유사도 지표를 사용해 쿼리 벡터에 가장 가까운 k개의 벡터를 찾습니다.
Elasticsearch kNN 검색을 사용하면 정확한 키워드 일치가 아니라 의미 기반으로 결과를 조회할 수 있습니다.

kNN 벡터 유사도 검색의 일반적인 활용 사례는 다음과 같습니다.

  • 검색

    • 시맨틱 텍스트 검색
    • 이미지 및 동영상 유사도
  • 추천

    • 상품 추천
    • 협업 필터링
    • 개인화된 콘텐츠 탐색
  • 분석

    • 이상 탐지
    • 패턴 매칭

Elasticsearch에서 kNN 검색을 실행하려면 다음이 필요합니다.

  • 데이터가 벡터화되어 있어야 합니다. Elasticsearch에서 NLP 모델을 사용하거나 Elasticsearch 외부에서 벡터를 생성할 수 있습니다.

    • 밀집 벡터에는 dense_vector 필드 타입을 사용하세요.
    • 쿼리 벡터는 문서 벡터와 차원이 같아야 하고 동일한 모델로 생성되어야 합니다.
    • 이미 벡터가 있나요? 직접 만든 밀집 벡터 사용하기를 참고하세요.
  • 필요한 인덱스 권한:

    • dense_vector 필드가 있는 인덱스를 생성하려면 create_index 또는 manage
    • 데이터를 추가하려면 create, index 또는 write
    • 인덱스를 검색하려면 read
Tip

고차원의 비압축 밀집 벡터(int4 또는 int8 양자화 전략)를 사용할 계획이 아니라면, 기본 타입의 Elasticsearch Serverless 프로젝트가 이 사용 사례에 적합합니다. Elasticsearch의 밀집 벡터 검색 > 범용 프로젝트와 벡터 최적화 프로젝트를 참고하세요.

Elasticsearch는 kNN 검색을 수행하는 여러 방법을 제공합니다. 어떤 방법을 쓸지는 필드 타입과 kNN을 다른 쿼리와 결합해야 하는지에 따라 달라집니다.

빠르고 확장 가능한 유사도 검색입니다. 대부분의 프로덕션 워크로드에 적합합니다. 근사 kNN 검색을 실행하는 방법은 세 가지이며, 지원하는 필드 타입이 각각 다릅니다.

방법 지원 필드 타입 사용 사례
최상위 knn 옵션 dense_vector 단독 kNN 검색 또는 점수 융합을 사용하는 하이브리드 검색
knn 쿼리 dense_vector, semantic_text bool 절 안에서 다른 쿼리와 조합 가능. semantic_text 필드에는 필수
knn retriever dense_vector 랭킹 및 결과 병합을 위해 retriever 파이프라인 안에서 사용
Tip

semantic_text 필드를 사용한다면, 가장 간단한 방법으로는 match 쿼리로 조회하고, 검색을 더 세밀하게 제어해야 할 때는 knn 쿼리를 사용하세요.

벡터 함수와 함께 script_score 쿼리를 사용합니다. 작은 데이터셋이나 정밀한 스코어링에 적합합니다. 정확 kNN을 참고하세요.

근사 kNN은 낮은 지연 시간과 좋은 정확도를 제공하는 반면, 정확 kNN은 정확한 결과를 보장하지만 대규모 데이터셋에서는 확장성이 떨어집니다. 정확 kNN에서는 script_score 쿼리가 벡터 함수를 계산하기 위해 일치하는 모든 문서를 스캔해야 하므로 검색 속도가 느려질 수 있습니다. 다만 쿼리를 사용해 함수에 전달되는 일치 문서 수를 제한하면 지연 시간을 개선할 수 있습니다. 데이터를 작은 문서 집합으로 필터링하면 이 방식으로도 좋은 검색 성능을 얻을 수 있습니다.

Warning

근사 kNN 검색에는 특정한 리소스 요구 사항이 있습니다. 예를 들어 HNSW의 경우, 효율적인 성능을 위해 모든 벡터 데이터가 노드의 페이지 캐시에 들어가야 합니다. 설정 팁은 근사 kNN 튜닝 가이드를 참고하세요.

근사 kNN 검색을 실행하려면 다음과 같이 하세요.

  1. 하나 이상의 dense_vector 필드를 매핑합니다. 근사 kNN 검색에는 다음 매핑 옵션이 필요합니다.

    • similarity 값. 이 값은 쿼리 벡터와 문서 벡터 간 유사도를 기준으로 문서에 점수를 매길 때 사용하는 유사도 지표를 결정합니다. 사용 가능한 지표 목록은 similarity 파라미터 문서를 참고하세요. similarity 설정의 기본값은 cosine입니다.
    				PUT image-index
    					{
      "mappings": {
        "properties": {
          "image-vector": {
            "type": "dense_vector",
            "dims": 3,
            "similarity": "l2_norm"
          },
          "title-vector": {
            "type": "dense_vector",
            "dims": 5,
            "similarity": "l2_norm"
          },
          "title": {
            "type": "text"
          },
          "file-type": {
            "type": "keyword"
          }
        }
      }
    }
    		
  2. 임베딩과 함께 데이터를 인덱싱합니다.

    				POST image-index/_bulk?refresh=true
    					{ "index": { "_id": "1" } }
    { "image-vector": [1, 5, -20], "title-vector": [12, 50, -10, 0, 1], "title": "moose family", "file-type": "jpg" }
    { "index": { "_id": "2" } }
    { "image-vector": [42, 8, -15], "title-vector": [25, 1, 4, -12, 2], "title": "alpine lake", "file-type": "png" }
    { "index": { "_id": "3" } }
    { "image-vector": [15, 11, 23], "title-vector": [1, 5, 25, 50, 20], "title": "full moon", "file-type": "jpg" }
    ...
    		
  3. knn 옵션 또는 knn 쿼리로 조회합니다.

    				POST image-index/_search
    					{
      "knn": {
        "field": "image-vector",
        "query_vector": [-5, 9, -12],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title", "file-type" ]
    }
    		

문서의 _score는 결과 관련성의 순위를 매기는 양의 32비트 부동소수점 숫자입니다. Elasticsearch kNN 검색에서 _score는 쿼리 벡터와 문서 벡터 사이에 선택된 벡터 유사도 지표로부터 산출됩니다. kNN 점수 계산 방식에 대한 자세한 내용은 similarity를 참고하세요.

Note

근사 kNN 검색 지원은 8.0 버전에서 추가되었습니다. 8.0 이전에는 dense_vector 필드가 매핑에서 index를 활성화하는 것을 지원하지 않았습니다. 8.0 이전에 dense_vector 필드로 인덱스를 생성했다면, 근사 kNN을 사용하기 위해 index: true(기본값)로 설정한 새 매핑으로 재인덱싱하세요.

근사 kNN의 경우, Elasticsearch는 밀집 벡터 값을 세그먼트별로 HNSW 그래프로 저장하거나, 세그먼트별로 DiskBBQ를 사용한 클러스터로 저장합니다. 이러한 근사 kNN 구조를 구축하는 작업은 연산 부하가 크기 때문에 벡터 인덱싱에 시간이 오래 걸릴 수 있습니다. 따라서 인덱스 및 벌크 작업에 대한 클라이언트 요청 타임아웃을 늘려야 할 수도 있습니다. 근사 kNN 튜닝 가이드는 검색 성능에 영향을 주는 인덱싱 성능, 사이징, 설정 간의 트레이드오프를 다룹니다.

검색 시점 파라미터 외에도, HNSW와 DiskBBQ는 그래프 구축 비용, 검색 속도, 정확도의 균형을 맞추는 인덱스 시점 설정을 제공합니다. dense_vector 매핑을 정의할 때 index_options를 사용해 이 파라미터를 설정하세요.

Tip

밀집 벡터 임베딩과 함께 semantic_text 필드 타입을 사용할 때도 필드에 직접 index_options를 설정할 수 있습니다. 예시는 index_options로 벡터 스토리지 최적화하기를 참고하세요.

				PUT image-index
					{
  "mappings": {
    "properties": {
      "image-vector": {
        "type": "dense_vector",
        "dims": 3,
        "similarity": "l2_norm",
        "index_options": {
          "type": "hnsw",
          "m": 32,
          "ef_construction": 100
        }
      }
    }
  }
}
		

최상위 knn 객체의 near_real_time 파라미터를 사용하면 근사 kNN 검색이 아직 검색용으로 최적화되지 않은 벡터를 포함할지 여부를 제어할 수 있습니다.

근사 kNN의 경우, Elasticsearch는 밀집 벡터 값을 세그먼트별로 HNSW 그래프 또는 DiskBBQ 클러스터로 저장합니다. 이러한 구조를 구축하는 작업은 연산 부하가 큽니다. 새로 인덱싱된 데이터에 대해 이 작업이 끝나기 전까지 해당 벡터는 근사 kNN 검색용으로 아직 최적화되지 않은 상태입니다.

기본값은 인덱스 모드에 따라 다릅니다. standard 모드를 사용하는 인덱스에서는 near_real_time의 기본값이 true입니다. vectordb_document 모드를 사용하는 인덱스에서는 기본값이 false입니다. 자세한 내용은 벡터 검색을 위한 인덱스 모드벡터 인덱스 모드를 참고하세요.

near_real_timetrue이면, kNN 검색은 최적화가 끝나기 전이라도 새로 인덱싱된 데이터의 벡터를 포함할 수 있습니다. 몇 초 전에 인덱싱한 문서가 결과에 포함될 수 있지만, 최적화가 진행 중인 동안에는 검색이 더 느려질 수 있습니다. 인덱싱 처리량보다 검색 신선도가 더 중요할 때, 예를 들어 새로 인덱싱된 문서가 kNN 결과에 즉시 나타나기를 사용자가 기대하는 대화형 워크플로에서 사용하세요.

near_real_timefalse이면, kNN 검색은 아직 완전히 최적화되지 않은 벡터를 건너뜁니다. 이는 대규모 벡터 워크로드의 인덱싱 처리량을 개선하지만, 백그라운드 최적화가 완료될 때까지 새로 인덱싱된 문서는 kNN 검색 결과에서 제외됩니다. 대량의 벡터를 인덱싱하고 있고 해당 문서가 근사 kNN으로 검색되기까지 약간의 지연을 감수할 수 있을 때 사용하세요.

다음 예시는 기본 동작을 재정의하고 준실시간 검색을 활성화하여, 근사 kNN 쿼리가 아주 최근에 인덱싱된 벡터도 포함할 수 있게 합니다.

				POST my-vector-index/_search
					{
  "knn": {
    "field": "image-vector",
    "query_vector": [-5, 9, -12],
    "k": 10,
    "num_candidates": 100,
    "near_real_time": true
  }
}
		

결과를 수집하기 위해, kNN API는 먼저 샤드별로 num_candidates개의 근사 이웃을 찾고, 쿼리 벡터와의 유사도를 계산한 뒤, 샤드별 상위 k개를 선택하고, 이를 병합해 전역 상위 k개의 최근접 이웃을 만듭니다.

HNSW 인덱스에서는 num_candidates가 검색 시점의 주요 속도/정확도 조절 수단입니다.

  • 재현율과 정확도를 높이려면 num_candidates를 늘리세요(지연 시간 증가를 감수).
  • 더 빠른 쿼리를 원하면 num_candidates를 줄이세요(정확도 저하 가능).

DiskBBQ(bbq_disk) 인덱스에서는 visit_percentage를 사용해 검색 중 방문하는 전체 벡터의 비율도 제어할 수 있습니다. visit_percentage0부터 100까지의 값을 받으며, 0.5(0.5퍼센트) 같은 소수 값도 허용합니다.

  • 시작 값으로는 3(3%)이 적절합니다.
  • 재현율과 정확도를 높이려면 visit_percentage를 늘리세요(지연 시간 증가를 감수).
  • 더 빠른 쿼리를 원하면 visit_percentage를 줄이세요(정확도 저하 가능).

검색이 HNSW 인덱스와 DiskBBQ 인덱스를 모두 대상으로 할 때는, visit_percentagenum_candidates와 함께 사용해 두 인덱스 타입 모두에서 성능과 재현율을 조정하세요.

양자화가 적용된 경우, rescore_vector는 추가적인 속도/정확도 조절 수단이 됩니다. 근사 검색 이후 원본 벡터를 사용해 더 큰 후보 집합의 순위를 다시 매깁니다.

  • 정확도를 높이려면 rescore_vector.oversample을 늘리세요(지연 시간 증가를 감수).
  • 더 빠른 쿼리를 원하면 rescore_vector.oversample을 줄이세요(정확도 저하 가능).
  • 자세한 동작과 사용 지침은 양자화 벡터의 오버샘플링과 리스코어링을 참고하세요.

근사 kNN 검색 API는 float 벡터와 함께 byte(int8) 값 벡터도 지원합니다. element_typebyte로 설정되고 인덱싱이 활성화된 dense_vector 필드를 검색하려면 knn 옵션을 사용하세요. 바이트 벡터는 메모리 사용량을 줄이고 대규모 벡터 유사도 검색에서 캐시 효율을 개선할 수 있습니다.

  1. element_typebyte로 설정하고 인덱싱을 활성화하여 하나 이상의 dense_vector 필드를 명시적으로 매핑합니다.

    				PUT byte-image-index
    					{
      "mappings": {
        "properties": {
          "byte-image-vector": {
            "type": "dense_vector",
            "element_type": "byte",
            "dims": 2
          },
          "title": {
            "type": "text"
          }
        }
      }
    }
    		
  2. 모든 벡터 값이 [-128, 127] 범위의 정수인지 확인하며 데이터를 인덱싱합니다.

    				POST byte-image-index/_bulk?refresh=true
    					{ "index": { "_id": "1" } }
    { "byte-image-vector": [5, -20], "title": "moose family" }
    { "index": { "_id": "2" } }
    { "byte-image-vector": [8, -15], "title": "alpine lake" }
    { "index": { "_id": "3" } }
    { "byte-image-vector": [11, 23], "title": "full moon" }
    		
  3. query_vector 값이 [-128, 127] 범위의 정수인지 확인하며 knn 옵션으로 검색을 실행합니다.

    				POST byte-image-index/_search
    					{
      "knn": {
        "field": "byte-image-vector",
        "query_vector": [-5, 9],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title" ]
    }
    		

참고: 표준 바이트 배열 외에도 query_vector 파라미터에 16진수로 인코딩된 문자열 값을 제공할 수 있습니다. 예를 들어 위 검색 요청은 다음과 같이 표현할 수도 있으며 동일한 결과를 반환합니다.

				POST byte-image-index/_search
					{
  "knn": {
    "field": "byte-image-vector",
    "query_vector": "fb09",
    "k": 10,
    "num_candidates": 100
  },
  "fields": [ "title" ]
}
		

float 벡터를 제공하면서도 byte 벡터의 메모리 절감 효과를 얻고 싶다면 양자화 기능을 사용하세요. 양자화를 사용하면 float 벡터를 제공하더라도 내부적으로는 byte 벡터로 인덱싱됩니다. 또한 원본 float 벡터도 인덱스에 그대로 보존됩니다.

Note

float 벡터의 기본 인덱스 타입은 제품 버전과 벡터 차원에 따라 bbq_hnsw 또는 int8_hnsw입니다. byte와 같은 다른 element 타입은 양자화 없이 기본적으로 일반 hnsw를 사용합니다. 밀집 벡터 필드 타입을 참고하세요.

기본 양자화 전략을 사용하거나 인덱스 옵션을 지정할 수 있습니다. 예를 들어 int8_hnsw를 사용해 보겠습니다.

				PUT quantized-image-index
					{
  "mappings": {
    "properties": {
      "image-vector": {
        "type": "dense_vector",
        "element_type": "float",
        "dims": 2,
        "index": true,
        "index_options": {
          "type": "int8_hnsw"
        }
      },
      "title": {
        "type": "text"
      }
    }
  }
}
		
  1. float 벡터를 인덱싱합니다.

    				POST quantized-image-index/_bulk?refresh=true
    					{ "index": { "_id": "1" } }
    { "image-vector": [0.1, -2], "title": "moose family" }
    { "index": { "_id": "2" } }
    { "image-vector": [0.75, -1], "title": "alpine lake" }
    { "index": { "_id": "3" } }
    { "image-vector": [1.2, 0.1], "title": "full moon" }
    		
  2. knn 옵션으로 검색을 실행합니다. 검색 시 float 벡터는 자동으로 byte 벡터로 양자화됩니다.

    				POST quantized-image-index/_search
    					{
      "knn": {
        "field": "image-vector",
        "query_vector": [0.1, -2],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title" ]
    }
    		

원본 float 벡터가 양자화 인덱스와 함께 보존되므로, 이를 리스코어링에 사용할 수 있습니다. int8_hnsw 인덱스로 후보를 빠르게 조회한 다음, 원본 float 벡터로 상위 k개 히트를 다시 스코어링하는 방식입니다. 이렇게 하면 빠른 검색과 정확한 스코어링이라는 두 가지 장점을 모두 얻을 수 있습니다.

				POST quantized-image-index/_search
					{
  "knn": {
    "field": "image-vector",
    "query_vector": [0.1, -2],
    "k": 15,
    "num_candidates": 100
  },
  "fields": [ "title" ],
  "rescore": {
    "window_size": 10,
    "query": {
      "rescore_query": {
        "script_score": {
          "query": {
            "match_all": {}
          },
          "script": {
            "source": "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
            "params": {
              "query_vector": [0.1, -2]
            }
          }
        }
      }
    }
  }
}
		

원본 벡터를 4바이트 값으로 저장하는 대신, element_type: bfloat16을 사용해 각 차원을 2바이트 값으로 저장할 수 있습니다. 인덱싱하는 벡터가 이미 bfloat16 정밀도이거나, 벡터 데이터 저장에 필요한 디스크 공간을 줄이고 싶을 때 유용합니다. 이 element 타입을 사용하면 Elasticsearch는 벡터를 인덱싱할 때 4바이트 float 값을 2바이트 bfloat16 값으로 자동 반올림합니다.

bfloat16의 정밀도가 낮기 때문에, 인덱스에서 조회한 벡터는 원래 인덱싱한 값과 약간 다를 수 있습니다.

kNN 검색 API는 필터로 벡터 유사도 검색을 제한하는 기능을 지원합니다. 요청은 필터 쿼리도 함께 만족하는 상위 k개의 최근접 이웃을 반환하므로, Elasticsearch에서 대상을 좁힌 사전 필터링 근사 kNN을 수행할 수 있습니다.

다음 요청은 file-type 필드로 필터링한 근사 kNN 검색을 수행합니다.

				POST image-index/_search
					{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  },
  "fields": ["title"],
  "_source": false
}
		
Note

필터는 근사 kNN 검색 도중에 적용되어 k개의 일치 문서가 반환되도록 보장합니다. 반면 사후 필터링은 근사 kNN 단계 이후에 필터를 적용하므로, 관련 문서가 충분히 존재하더라도 k개보다 적은 결과를 반환할 수 있습니다.

HNSW 인덱스를 사용하는 근사 kNN 검색에서는 필터를 적용하면 성능이 저하될 수 있습니다. 엔진이 필터를 만족하면서 num_candidates에 도달할 만큼의 후보를 모으기 위해 그래프를 더 많이 탐색해야 하기 때문입니다. 이는 필터가 엄격할수록 쿼리가 빨라지는 경우가 많은 기존 쿼리 필터링과 대조적입니다.

큰 성능 저하를 피하기 위해 Lucene은 세그먼트별로 다음 전략을 구현합니다.

  • 필터링된 문서 수가 num_candidates 이하이면, 검색은 HNSW 그래프를 건너뛰고 필터링된 문서에 대해 브루트포스 검색을 수행합니다.
  • HNSW 그래프를 탐색하는 중에 탐색한 노드 수가 필터를 만족하는 문서 수를 초과하면, 검색은 그래프 탐색을 중단하고 필터링된 문서에 대한 브루트포스 검색으로 전환합니다.

knn 옵션을 표준 query와 결합해 하이브리드 검색을 수행할 수 있습니다. 이렇게 하면 벡터 유사도를 어휘 기반 관련성, 필터, 집계와 함께 활용할 수 있습니다.

				POST image-index/_search
					{
  "query": {
    "match": {
      "title": {
        "query": "mountain lake",
        "boost": 0.9
      }
    }
  },
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "boost": 0.1
  },
  "size": 10
}
		

이 검색은 전역 상위 k = 5개의 벡터 매치를 찾고, 이를 match 쿼리의 매치와 결합한 뒤, 최종적으로 점수가 가장 높은 10개의 결과를 반환합니다. knnquery의 매치는 둘 사이에 불리언 or을 취한 것처럼 논리합으로 결합됩니다. 상위 k개의 벡터 결과는 모든 인덱스 샤드에 걸친 전역 최근접 이웃을 나타냅니다.

각 결과의 점수는 knn 점수와 query 점수의 합입니다. boost 값을 지정해 합산 시 각 점수에 가중치를 줄 수 있습니다. 앞의 예시에서 점수는 다음과 같이 계산됩니다.

score = 0.9 * match_score + 0.1 * knn_score
		

knn 옵션은 aggregations와 함께 사용할 수도 있습니다. 일반적으로 Elasticsearch는 검색에 일치하는 모든 문서에 대해 집계를 계산합니다. 따라서 근사 kNN 검색에서는 상위 k개의 최근접 문서에 대해 집계가 계산됩니다. 검색에 query도 포함되어 있다면, knn 매치와 query 매치를 합친 집합에 대해 집계가 계산됩니다.

Tip

최소한의 설정으로 시작하고 싶나요? semantic_text 필드 타입은 이러한 벡터 검색 구현을 합리적인 기본값과 자동 모델 관리로 추상화합니다. 대부분의 사용자에게 권장되는 방식입니다. semantic_text에 대해 자세히 알아보기.

kNN 검색을 사용하면 미리 배포된 텍스트 임베딩 모델을 활용해 시맨틱 검색을 수행할 수 있습니다. 시맨틱 검색은 검색어를 문자 그대로 일치시키는 대신, 검색 쿼리의 의도와 문맥적 의미를 바탕으로 결과를 조회합니다.

내부적으로 텍스트 임베딩 NLP 모델은 입력 쿼리 문자열(model_text로 제공)을 밀집 벡터로 변환합니다. 이 쿼리 벡터는 동일한 텍스트 임베딩 머신러닝 모델로 생성된 밀집 벡터를 담고 있는 인덱스와 비교됩니다. 검색 결과는 모델이 학습한 바에 따라 의미적으로 유사합니다.

Important

시맨틱 검색을 수행하려면 다음이 필요합니다.

  • 검색 대상이 될 입력 데이터의 밀집 벡터 표현을 담고 있는 인덱스가 있어야 합니다.
  • 문서 벡터를 만들 때 사용한 것과 동일한 텍스트 임베딩 모델을 검색에도 사용해야 합니다.
  • 텍스트 임베딩 NLP 모델 배포가 시작되어 있어야 합니다.

query_vector_builder 객체에 배포된 텍스트 임베딩 모델 또는 모델 배포를 참조하고, 검색 문자열을 model_text로 제공하세요.

(...)
{
  "knn": {
    "field": "dense-vector-field",
    "k": 10,
    "num_candidates": 100,
    "query_vector_builder": {
      "text_embedding": {
        "model_id": "my-text-embedding-model",
        "model_text": "The opposite of blue"
      }
    }
  }
}
(...)
		
  1. 수행할 작업입니다. 이 경우에는 text_embedding입니다.
  2. 쿼리의 밀집 벡터를 생성하는 데 사용하는 텍스트 임베딩 모델의 ID입니다. 대상 인덱스의 문서 임베딩을 만든 것과 동일한 모델을 사용하세요. model_id 값으로 deployment_id를 제공할 수도 있습니다.
  3. 모델이 밀집 벡터 표현을 생성하는 데 사용할 쿼리 문자열입니다.
Tip

query_vector_builder 옵션(text_embedding, embedding, lookup)의 개요는 knn 검색용 쿼리 벡터 만들기를 참고하세요.

학습된 모델을 배포하고 이를 사용해 텍스트 임베딩을 만드는 방법에 대한 자세한 내용은 이 엔드투엔드 예시를 참고하세요.

하이브리드 검색 외에도, 하나의 요청에서 둘 이상의 kNN 벡터 필드를 검색할 수 있습니다.

				POST image-index/_search
					{
  "query": {
    "match": {
      "title": {
        "query": "mountain lake",
        "boost": 0.9
      }
    }
  },
  "knn": [ {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "boost": 0.1
  },
  {
    "field": "title-vector",
    "query_vector": [1, 20, -52, 23, 10],
    "k": 10,
    "num_candidates": 10,
    "boost": 0.5
  }],
  "size": 10
}
		

이 검색은 image-vector에 대해 전역 상위 k = 5개의 이웃을, title-vector에 대해 전역 상위 k = 10개의 이웃을 조회합니다. 이 벡터 결과 집합들은 match 쿼리의 매치와 결합되고, 전체 상위 10개 문서가 반환됩니다. 여러 knn 절과 query 절은 논리합(불리언 OR)으로 결합됩니다. 상위 k개의 벡터 결과는 모든 인덱스 샤드에 걸친 전역 최근접 이웃을 나타냅니다.

위와 같이 boost를 설정했을 때 문서의 스코어링은 다음과 같습니다.

score = 0.9 * match_score + 0.1 * knn_score_image-vector + 0.5 * knn_score_title-vector
		

kNN은 강력한 도구이지만, 언제나 k개의 최근접 이웃을 반환하려고 합니다. 따라서 knnfilter와 함께 사용하면, 관련 있는 문서를 모두 걸러내고 관련 없는 문서만 검색 대상으로 남을 수도 있습니다. 그런 상황에서도 knn은 그 이웃들이 벡터 공간에서 아무리 멀리 떨어져 있더라도 k개의 최근접 이웃을 반환하기 위해 최선을 다합니다.

이를 제어하려면 knn 절에서 similarity 파라미터를 사용하세요. 이 파라미터는 벡터가 매치로 인정받기 위해 충족해야 하는 최소 유사도 임계값을 설정합니다. 이 파라미터를 사용할 때 knn 검색 흐름은 다음과 같습니다.

  • 사용자가 제공한 filter 쿼리를 적용합니다.
  • 벡터 공간을 탐색해 k개의 후보를 모읍니다.
  • 설정된 similarity 임계값보다 유사도가 낮은 벡터를 제외합니다.
Note

similarity_score로 변환되기 이전, 그리고 boost가 적용되기 이전의 실제 유사도 값입니다.

설정된 각 유사도에 대해, 다음은 _score를 원래의 유사도로 되돌리는 방법을 보여줍니다. _score를 기준으로 필터링하려는 경우에 사용하세요.

  • l2_norm: sqrt((1 / _score) - 1)
  • cosine: (2 * _score) - 1
  • dot_product: (2 * _score) - 1
  • max_inner_product:
    • _score < 1: 1 - (1 / _score)
    • _score >= 1: _score - 1

예시: 이 쿼리는 주어진 query_vector로 검색하면서 filter를 적용하고, 매치가 지정한 similarity 임계값 이상이 되도록 요구합니다. 임계값 미만의 결과는 남은 이웃이 k개보다 적더라도 반환되지 않습니다.

				POST image-index/_search
					{
  "knn": {
    "field": "image-vector",
    "query_vector": [1, 5, -20],
    "k": 5,
    "num_candidates": 50,
    "similarity": 36,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  },
  "fields": ["title"],
  "_source": false
}
		

이 데이터셋에서 file-type = png인 유일한 문서는 벡터 [42, 8, -15]를 가지고 있습니다. [42, 8, -15][1, 5, -20] 사이의 l2_norm 거리는 41.412로, 설정된 similarity 임계값 36을 초과합니다. 따라서 이 검색은 히트를 반환하지 않습니다.

텍스트가 모델의 토큰 제한을 초과하면, 각 청크의 임베딩을 생성하기 전에 청킹을 수행해야 합니다. nested 필드와 dense_vector를 결합하면, 최상위 문서 메타데이터를 복사하지 않고도 가장 가까운 구절 검색을 수행할 수 있습니다.
중첩 kNN 쿼리는 score_mode=max만 지원한다는 점에 유의하세요.

다음은 벡터와 필터링용 최상위 메타데이터 일부를 저장하는 기본적인 구절 벡터 인덱스입니다.

				PUT passage_vectors
					{
    "mappings": {
        "properties": {
            "full_text": {
                "type": "text"
            },
            "creation_time": {
                "type": "date"
            },
            "paragraph": {
                "type": "nested",
                "properties": {
                    "vector": {
                        "type": "dense_vector",
                        "dims": 2,
                        "index_options": {
                            "type": "hnsw"
                        }
                    },
                    "text": {
                        "type": "text",
                        "index": false
                    },
                    "language": {
                        "type": "keyword"
                    }
                }
            },
            "metadata": {
                "type": "nested",
                "properties": {
                    "key": {
                        "type": "keyword"
                    },
                    "value": {
                        "type": "text"
                    }
                }
            }
        }
    }
}
		

위 매핑을 사용하면, 개별 구절 텍스트를 저장하면서 여러 구절 벡터를 인덱싱할 수 있습니다.

				POST passage_vectors/_bulk?refresh=true
					{ "index": { "_id": "1" } }
{ "full_text": "first paragraph another paragraph", "creation_time": "2019-05-04", "paragraph": [ { "vector": [ 0.45, 45 ], "text": "first paragraph", "paragraph_id": "1", "language": "EN" }, { "vector": [ 0.8, 0.6 ], "text": "another paragraph", "paragraph_id": "2", "language": "FR" } ], "metadata": [ { "key": "author", "value": "Jane Doe" }, { "key": "source", "value": "Internal Memo" } ] }
{ "index": { "_id": "2" } }
{ "full_text": "number one paragraph number two paragraph", "creation_time": "2020-05-04", "paragraph": [ { "vector": [ 1.2, 4.5 ], "text": "number one paragraph", "paragraph_id": "1", "language": "EN" }, { "vector": [ -1, 42 ], "text": "number two paragraph", "paragraph_id": "2", "language": "EN" }] , "metadata": [ { "key": "author", "value": "Jane Austen" }, { "key": "source", "value": "Financial" } ] }
		

쿼리는 일반적인 kNN 검색과 매우 유사해 보일 것입니다.

				POST passage_vectors/_search
					{
    "fields": ["full_text", "creation_time"],
    "_source": false,
    "knn": {
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2
    }
}
		

중첩 벡터가 총 4개인데도 응답은 여전히 두 개의 문서를 반환한다는 점에 유의하세요. 중첩 밀집 벡터에 대한 kNN 검색은 항상 최상위 문서 단위로 상위 결과를 다양화합니다. "k"개의 최상위 문서가 반환되며, 각 문서는 가장 가까운 구절 벡터(예: "paragraph.vector")를 기준으로 점수가 매겨집니다.

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                }
            },
            {
                "_index": "passage_vectors",
                "_id": "2",
                "_score": 0.9997144,
                "fields": {
                    "creation_time": [
                        "2020-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "number one paragraph number two paragraph"
                    ]
                }
            }
        ]
    }
}
		

중첩 kNN 검색에서 메타데이터로 필터링하고 싶나요? knn 절에 filter를 추가하세요.

올바른 결과를 얻으려면, 각 개별 필터는 다음 중 하나를 대상으로 해야 합니다.

  • 최상위 메타데이터
  • nested 메타데이터
    Note

    하나의 knn 검색에 여러 필터를 포함할 수 있습니다. 일부는 최상위 메타데이터를, 일부는 중첩 메타데이터를 대상으로 할 수 있습니다.

				POST passage_vectors/_search
					{
    "fields": [
        "creation_time",
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [0.45, 45],
        "field": "paragraph.vector",
        "k": 2,
        "filter": {
            "range": {
                "creation_time": {
                    "gte": "2019-05-01",
                    "lte": "2019-05-05"
                }
            }
        }
    }
}
		

최상위 creation_time 필터를 적용하면, 지정한 범위에 들어가는 문서는 하나뿐입니다.

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                }
            }
        ]
    }
}
		

다음 쿼리는 중첩 메타데이터 필터를 적용합니다. 상위 문서에 점수를 매길 때 "paragraph.language"가 "EN"인 중첩 벡터만 고려합니다.

				POST passage_vectors/_search
					{
    "fields": [
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [0.45, 45],
        "field": "paragraph.vector",
        "k": 2,
        "filter": {
            "match": {
                "paragraph.language": "EN"
            }
        }
    }
}
		

다음 예시는 두 개의 필터를 결합합니다. 하나는 중첩 메타데이터에, 다른 하나는 최상위 메타데이터에 적용됩니다. 상위 문서는 "paragraph.language": "EN"인 벡터이면서 그 상위 문서가 지정된 기간 범위에 속하는 경우에만 점수가 매겨집니다.

				POST passage_vectors/_search
					{
    "fields": [
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [0.45,45],
        "field": "paragraph.vector",
        "k": 2,
        "filter": [
            {"match": {"paragraph.language": "EN"}},
            {"range": { "creation_time": { "gte": "2019-05-01", "lte": "2019-05-05"}}}
        ]
    }
}
		

중첩 knn 검색은 형제 중첩 필드에 대한 사전 필터링도 지원합니다. 예를 들어 "paragraphs"와 "metadata"가 중첩 필드일 때, "paragraphs.vector"를 검색하면서 "metadata.key"와 "metadata.value"로 필터링할 수 있습니다.

				POST passage_vectors/_search
					{
    "fields": [
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [0.45, 45],
        "field": "paragraph.vector",
        "k": 2,
        "filter": {
            "nested": {
                "path": "metadata",
                "query": {
                    "bool": {
                        "must": [
                            { "match": { "metadata.key": "author" } },
                            { "match": { "metadata.value": "Doe" } }
                        ]
                    }
                }
            }
        }
    }
}
		
Note

형제 중첩 필드로 필터링할 때는 "inner_hits" 조회가 지원되지 않습니다.

매치된 각 상위 문서에서 가장 가까운 구절을 추출하려면, knn 절에 inner_hits를 추가하세요.

Note

여러 knn 절과 함께 inner_hits를 사용할 때는, 검색 요청을 실패시키는 이름 충돌을 피하기 위해 각 절에 고유한 inner_hits.name을 설정하세요.

				POST passage_vectors/_search
					{
    "fields": [
        "creation_time",
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2,
        "inner_hits": {
            "_source": false,
            "fields": [
                "paragraph.text"
            ],
            "size": 1
        }
    }
}
		

이제 결과에는 검색 시 찾은 가장 가까운 단락이 포함됩니다.

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                },
                "inner_hits": {
                    "paragraph": {
                        "hits": {
                            "total": {
                                "value": 2,
                                "relation": "eq"
                            },
                            "max_score": 1.0,
                            "hits": [
                                {
                                    "_index": "passage_vectors",
                                    "_id": "1",
                                    "_nested": {
                                        "field": "paragraph",
                                        "offset": 0
                                    },
                                    "_score": 1.0,
                                    "fields": {
                                        "paragraph": [
                                            {
                                                "text": [
                                                    "first paragraph"
                                                ]
                                            }
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                }
            },
            {
                "_index": "passage_vectors",
                "_id": "2",
                "_score": 0.9997144,
                "fields": {
                    "creation_time": [
                        "2020-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "number one paragraph number two paragraph"
                    ]
                },
                "inner_hits": {
                    "paragraph": {
                        "hits": {
                            "total": {
                                "value": 2,
                                "relation": "eq"
                            },
                            "max_score": 0.9997144,
                            "hits": [
                                {
                                    "_index": "passage_vectors",
                                    "_id": "2",
                                    "_nested": {
                                        "field": "paragraph",
                                        "offset": 1
                                    },
                                    "_score": 0.9997144,
                                    "fields": {
                                        "paragraph": [
                                            {
                                                "text": [
                                                    "number two paragraph"
                                                ]
                                            }
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                }
            }
        ]
    }
}
		

Elasticsearch에서 dense_vector 필드와 inner_hits를 활용한 중첩 kNN 검색을 사용하면, 구조화되고 청킹된 문서에서 가장 관련성 높은 구절을 조회할 수 있습니다.

이 방식은 다음과 같은 경우에 적합합니다.

  • 콘텐츠를 단락, 섹션 또는 기타 중첩 구조로 청킹하는 경우.
  • 매치된 각 문서에서 가장 관련성 높은 중첩 섹션만 조회하고 싶은 경우.
  • Elastic의 시맨틱 검색 기능이 제공하는 semantic_text 필드에 의존하는 대신, 커스텀 모델로 직접 벡터를 생성하는 경우.

이 예시는 문서 제목용 벡터를 최상위에 저장하고, 개별 단락용 벡터 여러 개를 중첩 필드 안에 저장하는 인덱스를 생성합니다.

				PUT nested_vector_index
					{
  "mappings": {
    "properties": {
      "paragraphs": {
        "type": "nested",
        "properties": {
          "text": {
            "type": "text"
          },
          "vector": {
            "type": "dense_vector",
            "dims": 2,
            "index_options": {
              "type": "hnsw"
            }
          }
        }
      }
    }
  }
}
		

각 단락에 대한 벡터를 포함한 예시 문서를 추가합니다.

				POST _bulk
					{ "index": { "_index": "nested_vector_index", "_id": "1" } }
{ "paragraphs": [ { "text": "First paragraph", "vector": [0.5, 0.4] }, { "text": "Second paragraph", "vector": [0.3, 0.8] } ] }
{ "index": { "_index": "nested_vector_index", "_id": "2" } }
{ "paragraphs": [ { "text": "Another one", "vector": [0.1, 0.9] } ] }
		

이 예시는 관련성 높은 단락 벡터를 가진 문서를 검색합니다.

				POST nested_vector_index/_search
					{
  "_source": false,
  "knn": {
    "field": "paragraphs.vector",
    "query_vector": [0.5, 0.4],
    "k": 2,
    "num_candidates": 10,
    "inner_hits": {
      "size": 2,
      "name": "top_passages",
      "_source": false,
      "fields": ["paragraphs.text"]
    }
  }
}
		

inner_hits 블록은 각 최상위 문서 내에서 가장 관련성 높은 단락을 반환합니다. size 파라미터로 반환되는 매치 개수를 제어하세요. 쿼리에 여러 kNN 절이 포함되어 있다면, 응답에서 이름이 충돌하지 않도록 각 절에 고유한 name을 설정하세요.

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "nested_vector_index",
        "_id": "1",
        "_score": 1,
        "inner_hits": {
          "top_passages": {
            "hits": {
              "total": {
                "value": 2,
                "relation": "eq"
              },
              "max_score": 1,
              "hits": [
                {
                  "_index": "nested_vector_index",
                  "_id": "1",
                  "_nested": {
                    "field": "paragraphs",
                    "offset": 0
                  },
                  "_score": 1,
                  "fields": {
                    "paragraphs": [
                      {
                        "text": [
                          "First paragraph"
                        ]
                      }
                    ]
                  }
                },
                {
                  "_index": "nested_vector_index",
                  "_id": "1",
                  "_nested": {
                    "field": "paragraphs",
                    "offset": 1
                  },
                  "_score": 0.92955077,
                  "fields": {
                    "paragraphs": [
                      {
                        "text": [
                          "Second paragraph"
                        ]
                      }
                    ]
                  }
                }
              ]
            }
          }
        }
      },
      {
        "_index": "nested_vector_index",
        "_id": "2",
        "_score": 0.8535534,
        "inner_hits": {
          "top_passages": {
            "hits": {
              "total": {
                "value": 1,
                "relation": "eq"
              },
              "max_score": 0.8535534,
              "hits": [
                {
                  "_index": "nested_vector_index",
                  "_id": "2",
                  "_nested": {
                    "field": "paragraphs",
                    "offset": 0
                  },
                  "_score": 0.8535534,
                  "fields": {
                    "paragraphs": [
                      {
                        "text": [
                          "Another one"
                        ]
                      }
                    ]
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}
		
  1. 두 개의 문서가 쿼리에 일치했습니다.
  2. 가장 관련성 높은 단락을 기준으로 한 문서 점수입니다.
  3. 일치한 단락은 inner_hits 섹션에 표시됩니다.
  4. 쿼리에 일치한 실제 단락 텍스트입니다.

  • 교차 클러스터 검색에서 kNN 검색을 사용할 때는 ccs_minimize_roundtrips 옵션이 지원되지 않습니다.
  • Elasticsearch는 효율적인 kNN을 위해 HNSW 알고리즘을 사용합니다. 대부분의 근사 기법과 마찬가지로, HNSW는 속도를 위해 완벽한 정확도를 포기하므로 결과가 항상 진정한 k개의 최근접 이웃은 아닙니다.
Note

근사 kNN은 샤드 전체에서 전역 상위 k개 매치를 모으기 위해 항상 dfs_query_then_fetch 검색 타입을 사용합니다. kNN 검색에는 search_type을 명시적으로 설정할 수 없습니다.

kNN 검색에 양자화 벡터를 사용할 때, 다음 방법으로 결과를 선택적으로 리스코어링해 성능과 정확도의 균형을 맞출 수 있습니다.

  • 오버샘플링 — 샤드별로 더 많은 후보를 조회합니다.
  • 리스코어링 — 원본(비양자화) 벡터를 사용해 오버샘플링된 후보의 점수를 다시 계산합니다.

최종 점수는 원본 float 벡터로 계산되므로, 리스코어링은 다음을 결합합니다.

  • 양자화 벡터를 사용한 근사 검색의 성능 및 메모리 이점.
  • 상위 후보를 리스코어링할 때 원본 벡터를 사용하는 데서 오는 정확도.

모든 양자화는 어느 정도의 정확도 손실을 유발하며, 압축률이 높을수록 대체로 손실이 커집니다. 실제로는 다음과 같습니다.

  • int8은 일반적으로 리스코어링이 거의 또는 전혀 필요하지 않습니다.
  • int4는 정확도나 재현율을 높이기 위해 리스코어링의 이점을 보는 경우가 많으며, 1.5×–2× 오버샘플링으로 대부분의 손실을 회복할 수 있습니다.
  • bbq는 매우 큰 인덱스나 양자화에 특화되어 설계된 모델이 아니라면 대개 리스코어링이 필요합니다. 일반적으로 3×–5× 오버샘플링이면 충분하지만, 저차원 벡터나 양자화가 잘 되지 않는 임베딩에는 더 높은 값이 필요할 수 있습니다.

리랭킹을 자동으로 수행하려면 rescore_vector를 사용하세요. oversample 값을 지정하면 근사 kNN은 다음과 같이 동작합니다.

  • 샤드별로 num_candidates개의 후보를 조회합니다.
  • 원본 벡터를 사용해 샤드별 상위 k * oversample개 후보를 리스코어링합니다.
  • 리스코어링된 상위 k개 후보를 반환합니다.

다음은 oversample 파라미터와 함께 rescore_vector 옵션을 사용하는 예시입니다.

				POST image-index/_search
					{
  "knn": {
    "field": "image-vector",
    "query_vector": [-5, 9, -12],
    "k": 10,
    "num_candidates": 100,
    "rescore_vector": {
      "oversample": 2.0
    }
  },
  "fields": [ "title", "file-type" ]
}
		

이 예시는 다음과 같이 동작합니다.

  • 근사 kNN으로 상위 100개 후보를 검색합니다.
  • 원본 비양자화 벡터를 사용해 샤드별 상위 20개 후보(oversample * k)를 리스코어링합니다.
  • 리스코어링된 상위 10개(k) 후보를 반환합니다.
  • 모든 샤드의 리스코어링된 후보를 병합하고, 상위 10개(k) 결과를 반환합니다.

기본적으로 Elasticsearch는 리스코어링을 수행하기 위해 원본 벡터 데이터를 메모리로 읽어 들입니다. 벡터 데이터가 너무 커서 오프힙 메모리에 한 번에 다 들어가지 않으면 성능에 영향을 줄 수 있습니다. on_disk_rescore: true 인덱스 설정을 지정하면, Elasticsearch는 리스코어링 중에 디스크에서 직접 벡터 데이터를 읽습니다.

이 설정은 새로 인덱싱되는 벡터에만 적용됩니다. 인덱스의 모든 벡터에 이 옵션을 적용하려면, 설정을 변경한 후 벡터를 재인덱싱하거나 강제 병합해야 합니다.

다음 섹션에서는 리스코어링을 수행하는 추가적인 방법을 소개합니다.

샤드마다 리스코어링하지 않고, 모든 샤드의 상위 결과에 대해 리스코어링하고 싶을 때 이 옵션을 사용할 수 있습니다.

kNN 검색의 상위 결과를 리스코어링하려면 _search 요청의 rescore 섹션을 사용하세요.

다음은 오버샘플링과 함께 최상위 knn 검색을 사용하고 rescore로 결과를 리랭킹하는 예시입니다.

				POST /my-index/_search
					{
  "size": 10,
  "knn": {
    "query_vector": [0.04283529, 0.85670587, -0.51402352, 0],
    "field": "my_int4_vector",
    "k": 20,
    "num_candidates": 50
  },
  "rescore": {
    "window_size": 20,
    "query": {
      "rescore_query": {
        "script_score": {
          "query": {
            "match_all": {}
          },
          "script": {
            "source": "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)",
            "params": {
              "queryVector": [0.04283529, 0.85670587, -0.51402352, 0]
            }
          }
        }
      },
      "query_weight": 0,
      "rescore_query_weight": 1
    }
  }
}
		
  1. 반환할 결과 수입니다. 10개뿐이며 2배로 오버샘플링해 20개의 최근접 이웃을 모을 것입니다.
  2. KNN 검색에서 반환할 결과 수입니다. HNSW 그래프당 50개의 후보로 근사 KNN 검색을 수행하고 양자화 벡터를 사용해, 양자화 점수 기준 가장 유사한 20개 벡터를 반환합니다. 또한 이것은 최상위 knn 객체이므로, 리스코어링 전에 모든 샤드에서 전역 상위 20개 결과가 모입니다. rescore와 결합하면 2x 오버샘플링이 되며, 이는 양자화 점수 기준으로 20개의 최근접 이웃을 모은 뒤 정밀도가 더 높은 float 벡터로 리스코어링한다는 의미입니다.
  3. 리스코어링할 결과 수입니다. 모든 결과를 리스코어링하려면 k와 같은 값으로 설정하세요
  4. 결과를 리스코어링하는 스크립트입니다. Script score는 원래 제공된 float32 벡터와 직접 상호작용합니다.
  5. 원본 쿼리의 가중치입니다. 여기서는 원본 점수를 버립니다
  6. 리스코어 쿼리의 가중치입니다. 여기서는 리스코어 쿼리만 사용합니다

샤드마다 리스코어링하면서 rescore_vector 옵션이 제공하는 것보다 리스코어링을 더 세밀하게 제어하고 싶을 때 이 옵션을 사용할 수 있습니다.

knn 쿼리script_score 쿼리 로 샤드별 리스코어링을 사용하세요. 일반적으로 샤드별 리스코어링 양이 더 많아지지만, 연산 비용을 대가로 전체 재현율을 높일 수 있습니다.

				POST /my-index/_search
					{
  "size": 10,
  "query": {
    "script_score": {
      "query": {
        "knn": {
          "query_vector": [0.04283529, 0.85670587, -0.51402352, 0],
          "field": "my_int4_vector",
          "num_candidates": 20
        }
      },
      "script": {
        "source": "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)",
        "params": {
          "queryVector": [0.04283529, 0.85670587, -0.51402352, 0]
        }
      }
    }
  }
}
		
  1. 반환할 결과 수입니다
  2. 초기 검색을 수행하는 knn 쿼리이며, 샤드별로 실행됩니다
  3. 초기 근사 knn 검색에 사용할 후보 수입니다. 양자화 벡터를 사용해 검색하고 샤드별 상위 20개 후보를 반환한 뒤 점수를 매깁니다
  4. 결과에 점수를 매기는 스크립트입니다. Script score는 원래 제공된 float32 벡터와 직접 상호작용합니다.

정확 kNN 검색을 실행하려면 벡터 함수와 함께 script_score 쿼리를 사용하세요.

  1. 하나 이상의 dense_vector 필드를 명시적으로 매핑합니다. 해당 필드를 근사 kNN에 사용할 계획이 없다면 index 매핑 옵션을 false로 설정하세요. 인덱싱 속도를 크게 개선할 수 있습니다.

    				PUT product-index
    					{
      "mappings": {
        "properties": {
          "product-vector": {
            "type": "dense_vector",
            "dims": 5,
            "index": false
          },
          "price": {
            "type": "long"
          }
        }
      }
    }
    		
  2. 데이터를 인덱싱합니다.

    				POST product-index/_bulk?refresh=true
    					{ "index": { "_id": "1" } }
    { "product-vector": [230.0, 300.33, -34.8988, 15.555, -200.0], "price": 1599 }
    { "index": { "_id": "2" } }
    { "product-vector": [-0.5, 100.0, -13.0, 14.8, -156.0], "price": 799 }
    { "index": { "_id": "3" } }
    { "product-vector": [0.5, 111.3, -13.0, 14.8, -156.0], "price": 1099 }
    ...
    		
  3. 검색 API를 사용해 벡터 함수가 포함된 script_score 쿼리를 실행합니다.

    Tip

    벡터 함수에 전달되는 일치 문서 수를 제한하려면 script_score.query 파라미터에 필터 쿼리를 지정하는 것을 권장합니다. 필요하다면 이 파라미터에 match_all 쿼리를 사용해 모든 문서를 일치시킬 수도 있습니다. 다만 모든 문서를 일치시키면 검색 지연 시간이 크게 늘어날 수 있습니다.

    				POST product-index/_search
    					{
      "query": {
        "script_score": {
          "query" : {
            "bool" : {
              "filter" : {
                "range" : {
                  "price" : {
                    "gte": 1000
                  }
                }
              }
            }
          },
          "script": {
            "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
            "params": {
              "queryVector": [-0.5, 90.0, -10, 14.8, -156.0]
            }
          }
        }
      }
    }
    		

k-최근접 이웃(kNN) 검색은 유사도 지표로 측정했을 때 쿼리 벡터에 가장 가까운 k개의 벡터를 찾습니다.

kNN의 일반적인 활용 사례는 다음과 같습니다.

  • 자연어 처리(NLP) 알고리즘 기반의 관련성 랭킹
  • 상품 추천 및 추천 엔진
  • 이미지 또는 동영상 유사도 검색
Tip

Elasticsearch에 밀집 벡터 임베딩을 수집하는 방법을 알아보려면 실습 튜토리얼을 확인하세요.