Neural sparse ANN search
출처: 문서
본문
Neural sparse ANN 검색
3.3에 도입 {: .label .label-purple }
Neural sparse 근사 최근접 이웃(ANN) 검색은 정확도와 지연 시간의 균형을 맞춰 쿼리 효율성을 향상시켜요. rank_features 필드에 exact search를 수행하는 기존 neural sparse 검색과 달리, neural sparse ANN 검색은 sparse_vector 필드에 Spilled Clustering of Inverted Lists with Summaries for Maximum Inner Product Search(SEISMIC) 알고리즘을 사용해 근사 검색 기법으로 최적화된 쿼리 성능을 제공해요.
Neural sparse ANN 검색은 기존 neural sparse 검색에 비해 다음과 같은 장점을 제공해요:
- 쿼리 성능 향상: ≥90% recall 조건에서 two-phase 쿼리에 비해 상당한 쿼리 속도 향상을 달성하고, 데이터셋 크기가 커질수록 선형 이상의 성능 확장성을 보여줘요.
- 확장성: 단일 노드에서 데이터셋이 5천만 벡터 규모까지 확장되어도 일관된 쿼리 성능을 유지해요.
- 메모리 효율성: byte quantization을 사용해 인덱스 크기를 줄여요. 엔진에 따라 메모리는 circuit breaker가 적용된 JVM heap 캐시나 off-heap memory-mapped 파일로 관리되어 리소스 고갈을 방지해요.
- 하이브리드 방식: 세그먼트 크기에 따라 최적의 인덱싱 전략을 자동으로 선택하며, 인덱싱 성능에 미치는 영향은 최소화해요.
- 검색 유연성: 쿼리 파라미터를 사용해 높은 recall과 낮은 지연 시간 사이의 트레이드오프를 조정할 수 있어요.
- 엔진 선택: Lucene 엔진이나 off-heap 메모리에서 인덱스를 구축하고 검색하는 native 엔진을 사용해요. 자세한 내용은 엔진을 참조하세요.
sparse 검색의 효율성이 필요하지만, 기존 neural sparse 검색 방식이 대규모 환경에서 제공할 수 있는 수준보다 더 나은 성능이 필요하다면 neural sparse ANN 검색을 고려해 보세요:
- 대규모 애플리케이션: 쿼리 성능이 중요한, 수백만~수십억 개 문서를 담은 데이터셋.
- 높은 처리량 시나리오: 많은 쿼리 부하에서 빠른 응답 시간이 필요한 애플리케이션.
Neural sparse ANN 검색의 작동 방식
Neural sparse ANN 검색은 neural sparse 벡터의 인덱싱과 쿼리를 모두 최적화하기 위해 여러 기법을 구현해요.
인덱싱
인덱싱 단계에서 neural sparse ANN 검색은 다음과 같은 핵심 최적화를 구현해요:
-
Posting list 클러스터링: inverted index의 각 term에 대해 알고리즘이 다음 작업을 수행해요:
- 문서를 token weight 기준 내림차순으로 정렬해요.
- 가중치가 가장 높은 상위
n_postings개 문서만 유지해요. - 클러스터링 알고리즘을 적용해 유사한 문서들을 하나의 클러스터로 묶어요. native 엔진은
clustering_batch_size를 사용해 각 posting list를 배치로 나누고 각 배치를 개별적으로 클러스터링하는데, 이를 통해 빌드 시간이 길어지는 대가를 치르면서 인덱스 구축에 필요한 메모리를 줄여요. - 각 클러스터에 대해 가중치가 가장 높은 token만 남긴 요약 sparse 벡터를 생성해요.
-
Forward index 유지 관리: Neural sparse ANN 검색은 클러스터링된 inverted index와, 쿼리 처리 중 효율적인 접근을 위해 문서 ID별로 구성된 완전한 sparse 벡터를 저장하는 forward index를 모두 유지 관리해요.
쿼리 처리
쿼리 실행 시 neural sparse ANN 검색은 효율적인 검색 프로세스를 사용해요:
-
Token 수준 프루닝: 주어진 쿼리에 대해 모든 token이 가중치를 기준으로 정렬돼요. 가중치가 가장 높은 상위
top_n개 token만 유지되어 방문하는 posting list 수가 줄어들어요. -
클러스터 수준 프루닝: 알고리즘이 먼저 쿼리 벡터와 클러스터 요약 벡터 간의 dot product 점수를 계산해요. 동적 임계값을 넘는 점수를 가진 클러스터만 선택되어 상세하게 검토돼요.
-
문서 수준 스코어링: 선택된 클러스터에 대해 neural sparse ANN 검색은 해당 클러스터 내 개별 문서를 검토하고, forward index에서 가져온 문서 벡터와 쿼리 간의 정확한 dot product 점수를 계산해요.
이 접근 방식은 점수를 계산해야 하는 문서 수를 획기적으로 줄여서, 높은 정확도를 유지하면서 상당한 성능 향상을 이뤄요.
하이브리드 인덱싱
Neural sparse ANN 검색은 각 세그먼트의 문서 수에 따라 인덱싱 성능과 쿼리 성능의 균형을 맞추는 하이브리드 인덱싱 방식이에요:
-
approximate_threshold보다 문서 수가 적은 세그먼트: 클러스터링 없이 인덱싱되므로, 이런 세그먼트에 대한 쿼리는 일치하는 모든 문서에 점수를 매겨요. Lucene 엔진은 이런 세그먼트를 일반 neural sparse(rank_features) 세그먼트로 인덱싱하고 표준 neural sparse 쿼리로 조회해요. native 엔진은 자체 네이티브 형식으로 구축된, 동등한 inverted index로 이런 세그먼트를 인덱싱해요. -
approximate_threshold보다 문서 수가 많은 세그먼트: neural sparse ANN 세그먼트로 인덱싱되고 sparse ANN 쿼리로 조회돼요.
이 하이브리드 방식은 인덱싱 성능과 쿼리 속도의 균형을 맞춰요. 작은 세그먼트는 클러스터링 오버헤드를 피하고, 큰 세그먼트는 근사 검색 최적화의 이점을 얻어요. 시스템은 동일한 인덱스 내에서 기존 neural sparse 쿼리와 neural sparse ANN 쿼리를 모두 지원해 하위 호환성을 유지해요.
SEISMIC 알고리즘에 대한 자세한 내용은 Efficient Inverted Indexes for Approximate Retrieval over Learned Sparse Representations를 참조하세요.
엔진
3.9에 도입 {: .label .label-purple }
_엔진_은 neural sparse ANN 인덱스를 구축하고 그 인덱스에 쿼리를 실행하는 구현체예요. 두 엔진 모두 SEISMIC 알고리즘을 구현하고 동일한 알고리즘 및 쿼리 파라미터를 받아들이지만, 인덱스가 저장되는 위치와 메모리 관리 방식이 달라요.
OpenSearch는 다음 엔진을 지원해요:
- Lucene: 클러스터링된 posting list와 forward index를 JVM heap에 구축하고, 플러그인이 관리하는 캐시에서 쿼리를 처리해요. 기본 엔진이에요.
- Native: 인덱스를 디스크의 memory-mapped 파일로 구축하고, off-heap 메모리에서 쿼리를 처리해요.
필드의 method 객체에서 engine 파라미터를 사용해 엔진을 선택해요:
PUT /my-sparse-ann-index
{
"settings": {
"index": {
"sparse": true
}
},
"mappings": {
"properties": {
"sparse_embedding": {
"type": "sparse_vector",
"method": {
"name": "seismic",
"engine": "native",
"parameters": {
"n_postings": 4000,
"cluster_ratio": 0.1,
"forward_index": "per_block",
"clustering_batch_size": 1
}
}
}
}
}
}
{% include copy-curl.html %}
필드를 생성한 후에는 method 객체를 업데이트할 수 없어요. 엔진을 변경하려면 원하는 매핑으로 새 인덱스를 만들고 데이터를 reindex하세요.
{: .important}
엔진 비교
다음 표는 두 엔진을 비교해요.
| 특성 | Lucene 엔진 | native 엔진 |
|---|---|---|
engine 값 |
lucene |
native |
| 기본 활성화 여부 | 예 | 아니요. native 엔진 활성화를 참조하세요. |
| 쿼리 성능 | 기준선 | 더 높은 검색 처리량과 더 낮은 쿼리 지연 시간 |
| 인덱스 구축 성능 | 기준선 | 더 빠른 세그먼트 및 force merge 구축 |
| 인덱스가 저장되는 위치 | JVM heap, 플러그인이 관리하는 캐시 | 디스크의 memory-mapped 파일, off-heap 메모리에서 읽음 |
| 메모리 제한 요소 | plugins.neural_search.circuit_breaker.limit 설정, LRU 캐시 제거 방식 적용 |
운영 체제 페이지 캐시, 설정 가능한 제한 없음, OpenSearch에 의한 제거 없음 |
| 필요한 JVM heap | 노드가 서비스하는 sparse 세그먼트의 작업 세트에 비례 | 인덱스가 heap에 저장되지 않으므로 최소 |
| 디스크 레이아웃 | Lucene이 관리 | 쿼리 시점에 memory mapping되는 전용 엔진 파일로, 크기는 forward index 레이아웃에 따라 달라짐 |
| 필터링 | Post-filtering | Pre-filtering. 필터링 지원을 참조하세요. |
approximate_threshold보다 문서 수가 적은 세그먼트의 형식 |
rank_features, 표준 neural sparse 쿼리로 조회 |
엔진의 네이티브 형식으로 구축된, rank_features에 상응하는 inverted index |
| Warm Up 및 Clear Cache API | 지원 | 해당 없음 |
| sparse 메모리 통계 | 보고됨 | 보고되지 않음 |
엔진을 선택하는 방법에 대한 지침은 엔진 선택하기를 참조하세요.
Lucene 엔진
Lucene 엔진은 기본 엔진으로, 추가 설정이 필요 없어요. 클러스터링된 posting list와 forward index 데이터를 JVM heap에 저장해요. 메모리 사용량은 circuit breaker로 제한되고, 한도에 도달하면 캐시에서 데이터가 제거돼요. 자세한 내용은 메모리 및 캐싱 설정을 참조하세요.
인덱스가 heap에 저장되기 때문에, 대규모 환경에서 Lucene 엔진을 실행하는 노드에는 자신이 서비스하는 모든 sparse 세그먼트의 작업 세트를 담을 수 있을 만큼 큰 JVM heap이 필요해요.
Native 엔진
native 엔진은 SEISMIC 인덱스를 파일에 기록하고, OpenSearch는 쿼리 시점에 그 파일을 memory mapping한 뒤 그 자리에서 읽어요. 디스크상의 레이아웃이 곧 런타임 레이아웃이므로, 인덱스를 로드할 때 재구성되는 것은 없어요. 이로 인해 다음과 같은 결과가 생겨요:
- 인덱스가 JVM heap을 소비하지 않고 가비지 컬렉션 압박도 추가하지 않으므로, 노드가 상대적으로 작은 heap으로도 큰 sparse 인덱스를 서비스할 수 있어요.
- 인덱스 메모리는 회수 가능한 운영 체제 페이지 캐시이므로, 메모리 압박이 생기면 운영 체제가 이를 회수해요. circuit breaker나 제거(eviction) 정책은 개입하지 않아요.
- 세그먼트에 대한 첫 쿼리는 memory mapping을 설정하는 일회성 비용을 지불해요. 이후 쿼리는 이를 재사용해요. 이 비용은 세그먼트 크기가 커질수록 커져요.
native 엔진은 운영 체제 페이지 캐시에 의존하며, 인덱스가 사용하는 메모리 양을 제한하는 설정은 없어요. native 엔진에 맞춰 노드 크기를 정할 때는 다른 memory-mapped Lucene 데이터와 마찬가지로 페이지 캐시를 위해 충분한 RAM을 남겨 두세요.
native 엔진 활성화
native 엔진은 기본적으로 비활성화되어 있어요. 필드가 native 엔진을 사용하려면 다음 클러스터 설정 두 가지가 모두 true여야 해요:
| 설정 | Static/Dynamic | 기본값 | 설명 |
|---|---|---|---|
plugins.neural_search.sparse.native_engine_feature_enabled |
Static | true |
native 엔진을 사용할 수 있는지 여부. 이 설정은 static이므로 각 노드의 opensearch.yml에서 설정해야 하며, 변경하려면 노드 재시작이 필요해요. |
plugins.neural_search.sparse.native_engine_enabled |
Dynamic | false |
런타임에 native 엔진이 활성화되어 있는지 여부. |
native 엔진을 활성화하려면 다음 요청을 보내세요:
PUT _cluster/settings
{
"persistent": {
"plugins.neural_search.sparse.native_engine_enabled": true
}
}
{% include copy-curl.html %}
두 설정 중 하나라도 false이면 OpenSearch는 engine을 native로 설정한 필드를 생성하려는 시도, 기존 native 엔진 필드에 문서를 인덱싱하려는 시도, 그리고 그런 필드를 쿼리하려는 시도를 거절해요. 필드의 매핑이 native 엔진을 지정하고 있기 때문에 OpenSearch는 Lucene 엔진으로 대체하지 않아요.
native 엔진이 비활성화되어 있는 동안에는 OpenSearch가 세그먼트 flush 및 merge 과정에서 native 인덱스 구축을 건너뛰어요. 원본 벡터는 여전히 디스크에 기록되므로, 엔진을 다시 활성화한 뒤 해당 인덱스들을 force merge하면 native 인덱스가 다시 구축돼요. {: .note}
forward index 레이아웃 선택
forward_index 알고리즘 파라미터는 native 엔진이 forward index를 저장하는 방식을 제어해요. 필드의 method.parameters 객체에 지정하세요. 다음 표는 사용 가능한 레이아웃을 설명해요.
| 값 | 설명 | 트레이드오프 |
|---|---|---|
shared (기본값) |
해당 필드를 위한 하나의 연속된 forward index. | 디스크 사용량이 낮음. |
per_block |
각 블록의 벡터가 블록과 함께 인라인으로 저장되므로, 쿼리는 자신이 선택한 블록만 읽어요. | 쿼리 지연 시간이 낮고, 디스크 사용량이 높음. |
두 레이아웃 모두 memory mapping되며 동일한 quantization을 적용해요. forward_index 파라미터는 forward index 데이터를 어디에 배치할지를 선택하는 것이고, 데이터가 저장되는 정밀도는 두 레이아웃 모두 동일해요.
engine을 lucene으로 설정한 상태에서 shared가 아닌 forward_index 값을 지정하면 요청이 거절돼요.
{: .warning}
native 엔진 고려 사항
native 엔진을 선택하기 전에 다음 사항을 유의하세요:
- Warm Up 및 Clear Cache API는 Lucene 엔진의 캐시를 대상으로 동작하며, native 엔진에는 적용되지 않아요.
- Neural Search Stats API가 반환하는 sparse 메모리 통계는 Lucene 엔진 캐시 사용량만 보고해요. native 엔진 인덱스 메모리는 포함되지 않아요.
plugins.neural_search.circuit_breaker.limit설정은 native 엔진에는 아무런 영향을 주지 않아요.- native 엔진은 데이터가 로컬 파일 시스템에 저장된 인덱스를 필요로 해요.
1단계: 인덱스 생성
neural sparse ANN 검색을 사용하려면 인덱스 수준에서 sparse 설정을 활성화하고 sparse_vector를 필드 타입으로 사용해야 해요.
인덱스 설정
neural sparse ANN 검색 기능을 활성화하려면 index.sparse: true로 설정하세요:
PUT /my-sparse-ann-index
{
"settings": {
"index": {
"sparse": true,
"number_of_shards": 2,
"number_of_replicas": 1
}
},
"mappings": {
"properties": {
"sparse_embedding": {
"type": "sparse_vector",
"method": {
"name": "seismic",
"parameters": {
"n_postings": 4000,
"cluster_ratio": 0.1,
"summary_prune_ratio": 0.4,
"approximate_threshold": 1000000
}
}
},
"text": {
"type": "text"
}
}
}
}
{% include copy-curl.html %}
이 예제는 engine 파라미터를 생략하므로 필드는 기본값인 Lucene 엔진을 사용해요. 대신 native 엔진을 사용하려면 엔진을 참조하세요.
파라미터에 대한 자세한 내용은 Sparse vector를 참조하세요.
2단계: 데이터 수집
토큰이 정수와 그에 대응하는 가중치로 표현되는 sparse 임베딩을 사용해 문서를 수집합니다:
POST _bulk
{ "create": { "_index": "my-sparse-ann-index", "_id": "1" } }
{ "sparse_embedding": {"10": 0.85, "23": 1.92, "24": 0.67, "78": 2.54, "156": 0.73}, "text": "OpenSearch neural sparse search" }
{ "create": { "_index": "my-sparse-ann-index", "_id": "2" } }
{ "sparse_embedding": {"3": 1.22, "19": 0.11, "21": 0.35, "300": 1.74, "985": 0.96}, "text": "Machine learning algorithms" }
{% include copy-curl.html %}
ingest 파이프라인을 사용하면 토큰을 자동으로 정수 형식으로 변환할 수도 있습니다.
3단계: 인덱스 쿼리하기
neural_sparse 쿼리에 성능 튜닝용 method_parameters를 지정해 neural sparse ANN 검색을 수행합니다.
neural sparse ANN 검색을 two-phase 파이프라인과 함께 사용하지 마세요. {: .important}
자연어로 쿼리하기
자연어 텍스트로 쿼리합니다. 이 방식은 텍스트를 sparse vector로 변환하기 위해 배포된 sparse encoding 모델이 필요합니다:
GET /my-sparse-ann-index/_search
{
"query": {
"neural_sparse": {
"sparse_embedding": {
"query_text": "machine learning algorithms",
"model_id": "your_sparse_model_id",
"method_parameters": {
"k": 10,
"top_n": 10,
"heap_factor": 1.0
}
}
}
}
}
{% include copy-curl.html %}
raw vector로 쿼리하기
미리 계산해 둔 sparse vector로 쿼리합니다. 이때 토큰은 정수와 그에 대응하는 가중치로 지정합니다:
GET /my-sparse-ann-index/_search
{
"query": {
"neural_sparse": {
"sparse_embedding": {
"query_tokens": {
"1055": 1.7,
"2931": 2.3
},
"method_parameters": {
"k": 10,
"top_n": 6,
"heap_factor": 1.2
}
}
}
}
}
{% include copy-curl.html %}
쿼리 파라미터
| 파라미터 | 설명 |
|---|---|
k |
반환할 최근접 상위 결과의 개수 |
top_n |
가중치가 가장 높은 쿼리 토큰 중 유지할 개수 |
heap_factor |
recall과 성능 간의 트레이드오프를 제어 |
filter |
사전 필터링 또는 사후 필터링에 사용하는 선택적 Boolean 필터 |
쿼리 문법과 이 파라미터는 두 엔진 모두에서 동일합니다. 엔진은 필드 매핑에서 선택합니다. 자세한 내용은 엔진을 참조하세요. {: .note}
필터링 지원
Neural sparse ANN 검색은 필터링을 지원합니다. 필터가 k개보다 적은 문서와 일치하면 두 엔진 모두 필터링된 문서를 대상으로 exact search를 수행합니다. 그렇지 않으면 두 엔진이 서로 다른 지점에서 필터를 적용합니다:
- Lucene 엔진은 근사 검색 후에 필터를 적용합니다(post-filtering). 결과는 상위 매치와 필터의 교집합이므로, 선택 범위가 좁은 필터에서는
k개보다 적은 결과가 반환될 수 있습니다. - native 엔진은 검색 전에 필터를 후보 집합 형태로 푸시다운합니다(pre-filtering). 필터링된 집합 안에서 검색이 수행되므로 쿼리가 전체
k개의 결과를 반환할 수 있습니다.
자세한 내용은 neural sparse ANN 검색의 필터링을 참조하세요.
클러스터 설정
Neural sparse ANN 검색은 다음과 같은 클러스터 설정을 지원합니다.
스레드 풀 구성
클러스터링된 inverted index 구조를 구축하는 작업은 많은 연산을 필요로 합니다. 기본적으로 알고리즘은 단일 스레드 스레드 풀을 사용해 클러스터를 구축합니다. 스레드 풀 크기를 늘리면 클러스터를 병렬로 구축할 수 있어, 더 많은 CPU 코어를 활용하고 인덱스 구축 시간을 단축할 수 있습니다. 이 설정은 두 엔진 모두에 적용됩니다.
스레드 풀 크기를 구성하려면 plugins.neural_search.sparse.algo_param.index_thread_qty 설정을 업데이트합니다:
PUT /_cluster/settings
{
"persistent": {
"plugins.neural_search.sparse.algo_param.index_thread_qty": 4
}
}
{% include copy-curl.html %}
메모리 및 캐싱 설정
Lucene 엔진은 알고리즘이 과도한 메모리를 사용하지 못하도록 막고 다른 OpenSearch 작업이 영향을 받지 않도록 보장하는 circuit breaker를 제공합니다. circuit_breaker.limit의 기본값은 10%입니다. 이 설정을 조정하면 알고리즘에 할당되는 총 메모리를 제어할 수 있습니다. 메모리 사용량이 정의된 한도에 도달하면 cache eviction이 발생해 가장 오래 사용되지 않은 데이터가 제거됩니다.
circuit breaker 한도를 높이면 더 많은 메모리를 사용할 수 있고 cache eviction 빈도가 줄어들지만, 다른 OpenSearch 작업에 영향을 줄 수 있습니다. 한도를 낮추면 더 안전하지만 cache eviction이 더 자주 발생할 수 있습니다.
circuit breaker 한도를 구성하려면 다음 요청을 보냅니다:
PUT _cluster/settings
{
"persistent": {
"plugins.neural_search.circuit_breaker.limit": "30%"
}
}
{% include copy-curl.html %}
circuit breaker는 Lucene 엔진의 JVM heap cache에 상한을 둡니다. native 엔진은 운영체제가 관리하는 memory-mapped file에 인덱스를 보관하므로 이 설정의 영향을 받지 않습니다. 자세한 내용은 native 엔진을 참조하세요. {: .note}
자세한 내용은 Neural Search 플러그인 설정을 참조하세요.
모니터링
Neural Search Stats API를 사용해 메모리 사용량과 쿼리 통계를 모니터링합니다.
sparse 메모리 통계는 Lucene 엔진의 캐시 사용량만 보고합니다. native 엔진의 인덱스 메모리는 운영체제 page cache에 보관되므로 이 통계에는 반영되지 않습니다. {: .note}
성능 튜닝
Neural sparse ANN 검색은 검색 정확도와 쿼리 속도 사이의 균형을 맞출 수 있는 여러 파라미터를 제공합니다. 포괄적인 튜닝 지침은 Neural sparse ANN 검색 성능 튜닝을 참조하세요.
다음 단계
- 쿼리 문법은 Neural sparse 쿼리를 참조하세요.
- 필드 타입 정보는 Sparse vector를 참조하세요.
- 성능 최적화는 Neural sparse ANN 검색 성능 튜닝을 참조하세요.
- 필터링 옵션은 neural sparse ANN 검색의 필터링을 참조하세요.