AWS 기술 블로그
Amazon OpenSearch Service로 미리캔버스의 듀얼 벡터 검색 도입과 성능 최적화
부제: 4,000만 건 디자인 리소스의 의미 기반 검색을 위한 듀얼 벡터 설계, 그리고 OpenSearch 2.19에서 마주한 메모리와 IOPS 병목과의 싸움
본 게시글은 미리디의 김민석, 최시온, 이동진, 김백규님과 함께 작성하였습니다.
미리디의 미리캔버스 소개
미리캔버스는 “누구나 쉽게, 함께 만드는 디자인”을 지향하는 실시간 협업 디자인 플랫폼입니다. 프레젠테이션, SNS 카드뉴스, 유튜브 썸네일, 포스터까지 다양한 시각 콘텐츠를 브라우저에서 바로 만들 수 있습니다. 현재 90만 개가 넘는 템플릿과 4,000만 개에 달하는 요소(이미지, 일러스트 등)를 제공하는데, 이 방대한 리소스에서 사용자가 원하는 디자인 요소를 얼마나 빠르고 정확하게 찾아 주느냐가 서비스 경험을 좌우합니다.
그래서 미리디는 단순한 키워드 매칭에서 한 걸음 더 나아가, 사용자의 의도까지 읽어내기 위해 10개가 넘는 벡터 모델을 함께 활용합니다. 이미지의 시각적 특징, 텍스트의 문맥적 의미, 스타일의 유사성을 각기 다른 모델이 나눠 맡는 방식입니다. 그런데 서비스가 커지고 사용자의 요구가 세밀해질수록 검색 쿼리도 복잡해졌고, 어느 순간부터 메모리와 성능을 압박하기 시작했습니다.
미리캔버스의 검색은 Amazon OpenSearch Service의 k-NN 플러그인 위에서 동작합니다. 이 글에서는 그 과정에서 부딪힌 리콜(Recall)과 인프라의 한계, 그리고 이를 넘어서려고 시도한 여러 최적화를 정리합니다. (이어지는 2부에서는 OpenSearch 2.19에서 3.3으로 메이저 버전을 올린 과정을 다룹니다.)
참고로 미리디는 앞선 마이그레이션 시리즈에서 데이터베이스 인프라를 한 차례 정비한 경험이 있습니다.
이번 글은 그 연장선에서, 이번에는 검색 인프라를 최적화한 이야기입니다.
배경: 복잡해지는 검색
서비스가 성장하면서, 벡터 모델 하나만으로는 사용자의 의도를 충분히 담아내기 어려운 검색 상황이 하나둘 나타났습니다.
예를 들어 크리스마스 초대장을 디자인하던 사용자가 “선물을 들고 있는 어린이” 요소를 찾는다고 해보겠습니다. 텍스트 의미만 이해하는 단일 벡터 검색으로도 “선물을 들고 있는 어린이” 요소 자체는 찾아냅니다. 문제는 그 요소가 지금 만들고 있는 초대장의 색감이나 분위기와 어울리는지는 알 수 없다는 점이었습니다. 결국 사용자는 검색 결과를 일일이 스크롤하며 직접 골라야 했고, 마음에 드는 요소를 찾는 데 시간이 오래 걸리거나 끝내 어울리는 요소를 못 찾고 차선책으로 타협하는 일도 잦았습니다.
이 문제를 풀려면 검색어의 의미를 이해하는 시맨틱 벡터만으로는 부족했습니다. 지금 편집 중인 디자인의 시각적 스타일까지 이해하는 비주얼 벡터를 함께 써야 했습니다. 이렇게 한 번의 쿼리에서 벡터 두 개를 동시에 사용하는 듀얼 벡터 검색이 출발점이 되었습니다.

[Figure 1-1] 단일 벡터 검색 결과 — 검색어의 의미는 맞지만 현재 편집 중인 디자인의 스타일과 어울리지 않는 요소가 섞여 노출됨

[Figure 1-2] 듀얼 벡터 검색 결과 — 의미와 시각적 스타일을 동시에 만족하는 요소가 상위에 노출됨
메모리 압박의 심화
문제가 된 인덱스는 벡터 필드 5개가 매핑되어 있고, 각 필드마다 약 4,000만 건의 문서가 임베딩을 들고 있는 구조였습니다. 앞서 소개한 10개 이상의 모델 가운데 핵심 5개 모델의 임베딩이 이 인덱스에 벡터 필드로 매핑되어 있습니다. 벡터 기반 기능을 하나씩 추가할 때마다 인프라에 가해지는 부하는 단순히 더해지는 정도가 아니라 복합적으로 불어났습니다.
메모리가 모자라면 운영체제는 디스크 스왑을 시작하고, 이는 곧 디스크 I/O 병목으로 이어집니다. 미리디가 쓰던 Amazon OpenSearch Service 환경에서는 스토리지로 사용하는 Amazon EBS 볼륨의 IOPS 한도가 정해져 있었기 때문에, 벡터 검색 쿼리가 몰리는 순간 이 한도를 넘어서면서 IOPS Throttling이 발생했습니다.
Throttling이 한번 걸리면 검색이 느려지는 정도로 끝나지 않았습니다. 클러스터 전체 응답이 사실상 멈추다시피 했고, 인덱싱도 검색도 타임아웃이 났습니다. 새 벡터 기능을 검증하려고 성능 테스트를 돌리다가 이 현상이 발생하면서 테스트 자체를 중단해야 했던 적도 있습니다.

[Figure 2] 성능 테스트 중 발생한 IOPS Throttle — EBS 볼륨의 IOPS 한도를 초과하면서 throttle이 반복적으로 발생
리콜(Recall)의 한계
성능과는 별개로 검색 품질 자체에도 문제가 있었습니다. 벡터 검색에서 리콜은 “전체 정답 중 실제 검색 결과에 포함된 비율”을 뜻합니다. IVF/PQ에서는 nprobes(탐색할 클러스터 수)를 높이면 리콜이 좋아지지만, 그만큼 탐색 범위가 넓어져 레이턴시와 메모리 사용량이 함께 올라갑니다. HNSW도 ef_search나 m을 키워야 리콜이 좋아지는데, 똑같은 트레이드오프가 따라옵니다.
메모리가 빠듯한 상황에서는 이 파라미터들을 충분히 올릴 수가 없었고, 원하는 리콜에 못 미치는 벡터 필드가 하나둘 생기기 시작했습니다. 검색 품질을 높이려면 리콜을 올려야 하는데, 리콜을 올리면 인프라가 버티지 못하는 딜레마에 빠진 것입니다.
미리캔버스가 시도한 최적화들
버전을 올리기 전에, 지금 환경에서 할 수 있는 최적화를 최대한 시도했습니다. 크게 ① 듀얼 벡터 검색의 설계, ② 듀얼 벡터 쿼리의 구현, ③ 인프라 스펙 개선, 이렇게 세 방향이었습니다.
1. 듀얼 벡터 검색의 설계
듀얼 벡터 검색은 막상 구현해보니 생각보다 훨씬 까다로웠습니다. 벡터 두 개를 쿼리에 넣는다고 끝나는 게 아니라, 성능과 품질 양쪽에서 여러 겹의 문제를 풀어야 했습니다.
두 벡터(시맨틱 + 비주얼)를 결합하는 방법으로 가장 먼저 살펴본 건 Rescore 전략이었습니다. 1차 쿼리에서 시맨틱 벡터로 후보군을 추리고, 2차 rescore 단계에서 비주얼 벡터로 다시 정렬하는 방식입니다. 직관적이고 구현도 쉬워 보였지만, 실제로 적용해보니 한계가 분명해 결국 접었습니다.
1차 후보군의 품질 문제: Rescore는 “1차 후보군에 좋은 결과가 충분히 들어 있으면, 2차에서 비주얼 벡터로 재정렬하면 된다”는 전제 위에 서 있습니다. 그런데 시맨틱 벡터만 쓰는 1차 쿼리만으로는 현재 디자인과 시각적으로 어울리는 요소가 후보군에 충분히 들어오지 않았습니다. 후보군 자체에 어울리는 요소가 적으니 아무리 비주얼 벡터로 다시 정렬해도 원하는 결과가 나오지 않았고, 쿼리마다 품질 편차도 심했습니다.
`window_size`의 딜레마: 후보군 품질을 높이려면 rescore의 window_size를 키워 후보를 더 많이 확보해야 합니다. 하지만 window_size가 커질수록 1차 쿼리가 반환할 문서 수가 늘어 레이턴시가 급격히 올라갔고, 오히려 관련성 낮은 문서가 후보군에 더 많이 섞여 결과가 나빠지는 역설이 생겼습니다. OpenSearch에서 rescore의 window_size는 최대 10,000까지인데, 이 한도 안에서는 원하는 품질을 만들기 어려웠습니다.
`min_score` 도입 시도: 1차 후보군을 정제하려고 min_score 도입도 검토했습니다. 시맨틱 유사도가 일정 임계값 아래인 결과를 아예 빼면 후보군이 깔끔해질 거라 기대했죠. 그런데 OpenSearch의 k-NN 쿼리에서 min_score를 지정하면 k 기반 검색이 아니라 radial search 모드로 동작이 바뀝니다. radial search는 “임계값 이상의 모든 문서를 반환”하는 방식이라 결과 수를 예측할 수 없었습니다. 임계값을 높이면 통과하는 문서가 극단적으로 줄고, 낮추면 너무 많이 반환되어 레이턴시가 치솟았습니다. 결과 수를 제어할 수 없다는 점이 부담스러워 이 방법도 접었습니다.
최종 선택: 결국 rescore 대신, 한 번의 쿼리에서 시맨틱 벡터와 비주얼 벡터를 동시에 사용하는 듀얼 벡터 쿼리를 택했습니다.

[Figure 3] 현재 디자인을 기준으로 한 검색 결과 비교 (검색어: 사람) — Rescore(AS-IS) 방식 대비 듀얼 벡터(TO-BE) 방식이 현재 디자인의 스타일과 어울리는 결과를 상위에 더 많이 반환
듀얼 벡터 쿼리는 두 벡터 인덱스를 각각 k=200으로 탐색하니 단일 벡터보다 연산량이 늘어나는 건 사실입니다. 그런데 막상 테스트해보니 디스크 I/O는 rescore 방식과 비슷한 수준이었습니다. k-NN 벡터 검색의 특성 때문입니다. HNSW 그래프 탐색이나 IVF/PQ 거리 계산은 모두 k-NN 네이티브 메모리(off-heap)에서 이뤄져 디스크 I/O를 만들지 않습니다. 디스크 I/O가 생기는 구간은 필터 평가(inverted index 읽기)와 결과 반환(stored fields 읽기)인데, 두 방식 모두 같은 필터 조건을 쓰고 최종 반환 문서 수도 비슷했기 때문에 I/O에서는 의미 있는 차이가 없었습니다.
디스크 I/O는 비슷한데 검색 정확도는 듀얼 벡터가 확연히 좋았습니다. Rescore는 1차 후보군 품질에 좌우되는 구조적 한계가 있는 반면, 듀얼 벡터는 두 벡터가 처음부터 함께 후보를 고르기 때문에 의미와 스타일을 모두 만족하는 결과가 상위로 올라옵니다. 이것이 듀얼 벡터 쿼리를 최종 선택한 결정적인 이유였습니다.
2. 듀얼 벡터 쿼리의 구현
듀얼 벡터를 도입하자 곧바로 벡터 검색 성능이 문제로 떠올랐습니다. 4,000만 건에 이르는 요소 문서를 대상으로 k-NN 검색을 돌리면, 필터를 걸어도 탐색 대상이 워낙 방대해 레이턴시가 크게 치솟았습니다.
knn-filtering 도입: 탐색 범위를 좁히려고 OpenSearch의 knn-filtering 기능으로 BM25 선(先)필터를 적용했습니다. k-NN 쿼리의 filter 절에 BM25 키워드 매칭 조건을 넣어, 벡터 탐색에 들어가기 전에 후보 문서를 미리 좁히는 방식입니다. 그 결과 평균 107ms였던 레이턴시가 13ms로, 약 88% 줄었습니다.
선필터가 가져온 뜻밖의 효과: BM25 선필터의 효과는 성능에만 그치지 않았습니다. 키워드로 기본적인 관련성이 있는 문서만 추린 다음 시맨틱 벡터가 의미 기반으로 정렬하니, 후보군 자체의 품질이 눈에 띄게 좋아졌습니다. 후보군이 좋아지자 스타일 벡터의 재정렬도 제 힘을 발휘했고, 특히 롱테일 쿼리에서 정확도가 크게 올라가면서 채택률(사용자가 검색 결과에서 실제로 리소스를 고른 비율)이 상대적으로 약 20%(절대값으로는 +9.02%p) 높아졌습니다.
| 그룹 | 후보군 구성 | 채택률 |
| 그룹 0 (기존) | 키워드 필터 + 유저픽 후보군 | 43.80% |
| 그룹 1 (개선) | 키워드 필터 + SigLIP 후보군 | 52.82% |
| 차이 | +9.02%p |
[Table 1] BM25 선필터 후보군 구성에 따른 채택률 비교 — 키워드 필터에 SigLIP(비주얼 벡터) 후보군을 결합한 그룹 1이 기존 유저픽 후보군 대비 채택률 +9.02%p 향상
두 벡터 스코어의 결합: 두 벡터 모델은 임베딩 공간이 달라서 유사도 스코어의 스케일과 분포도 제각각입니다. 그래서 AI 리서치 팀에게 받은 두 모델의 점수 분포 데이터를 바탕으로, 수동 boost 조정으로 스코어 스케일을 맞췄습니다. 이 듀얼 벡터 앙상블 설계 덕분에 의미와 스타일을 적절히 결합한 검색 결과를 제공할 수 있었지만, 두 벡터 인덱스를 동시에 탐색하는 만큼 쿼리당 연산 비용도 함께 늘었습니다.
3. 인프라 스펙 개선
듀얼 벡터 검색으로 결과의 정확도와 속도는 크게 좋아졌지만, 근본적인 I/O 부하는 여전히 그대로였습니다.
진짜 원인 찾기: 처음에는 벡터 두 개를 동시에 탐색하는 듀얼 벡터 자체의 연산 부하를 원인으로 의심했습니다. 그런데 지표를 분석해보니 이상한 점이 있었습니다. 레이턴시와 IOPS는 크게 뛰었는데, CPU 사용률은 이전과 큰 차이가 없었습니다. 쿼리 연산이 병목이었다면 CPU도 같이 치솟아야 정상입니다. CPU는 안정적인데 I/O만 폭증한다는 것은, 쿼리 부하가 아니라 메모리 부족에서 비롯된 디스크 I/O 병목을 의심해야 한다는 신호였습니다.
실제로 도입 전후를 클러스터 지표로 비교하면 이 패턴이 분명히 드러납니다.

[Figure 4-1] 듀얼 벡터 도입 전 클러스터 지표 (8/11~8/16) — CPU는 안정적이고 IOPS Throttle은 발생하지 않음

[Figure 4-2] 듀얼 벡터 도입 후 클러스터 지표 (8/26~8/29) — CPU 증가 폭은 작지만 Read IOPS가 급증하고 IOPS Throttle이 신규 발생
| 지표 | 듀얼 벡터 도입 전 (8/11~8/16) | 듀얼 벡터 도입 후 (8/26~8/29) | 변화 |
| CPU (AVG) | 10.9% | 13.3% | +2.4%p |
| CPU (MAX) | 28.8% | 32.5% | +3.7%p |
| Search Latency (AVG) | 16.6 ms | 17.5 ms | +5% |
| Search Latency (MAX) | 19.7 ms | 22.4 ms | +14% |
| Read IOPS (AVG) | 4,040 | 4,490 | +11% |
| Read IOPS (MAX) | 4,760 | 6,300 | +32% |
| IOPS Throttle | 0 | AVG 0.088 / MAX 6.67 | 신규 발생 |
[Table 2] 듀얼 벡터 도입 전후 클러스터 지표 비교 — CPU 증가 폭은 작은 반면 Read IOPS는 크게 늘고 IOPS Throttle이 신규 발생하여, 병목 원인이 쿼리 연산이 아닌 메모리 부족(디스크 I/O)임을 시사
이 가설을 확인하려고 GET /_plugins/_knn/stats로 k-NN 네이티브 메모리 상태부터 살펴봤습니다.
graph_memory_usage_percentage는 16% 수준이었습니다. FAISS 엔진이 네이티브 힙에 올린 벡터 인덱스 구조는 멀쩡히 로드되어 있었고, 서킷 브레이커도 발동하지 않은 상태였습니다.
그래서 이번엔 실제 디스크 I/O를 일으키는 OS 레벨 지표를 확인했습니다. GET _nodes/stats/os,jvm,fs를 조회해보니, 모든 노드의 OS 메모리 사용률이 99%에 달했고 급기야 swap 메모리까지 과도하게 끌어다 쓰고 있었습니다.
원인은 _source에 저장된 벡터 데이터였습니다. 이 데이터가 인덱스 전체 크기를 키운 것입니다.
Lucene은 하나의 세그먼트 안에서도 데이터를 용도별로 다른 파일에 나눠 저장합니다. 검색에 쓰이는 역인덱스(inverted index, .tip/.tim)와 doc values(.dvd)는 query phase에서, 원본 문서를 담은 stored fields(.fdt)는 fetch phase에서 서로 다른 타이밍에 페이지 캐시로 올라옵니다.
이 파일들은 물리적으로 떨어져 있지만, 페이지 캐시에 여유가 있으면 한 번 읽은 데이터가 캐시에 남아 다음 쿼리부터는 디스크를 타지 않습니다. 문제는 캐시가 부족할 때입니다. query phase에서 inverted index를 읽으면 stored fields가 밀려나고, fetch phase에서 stored fields를 읽으면 다시 inverted index가 밀려나는 스래싱(thrashing)이 반복됩니다. 그 결과 매 쿼리마다 디스크 읽기가 일어났고, 이것이 IOPS 폭증으로 이어졌습니다.
인스턴스 스펙 조정: 원인이 OS 레벨의 메모리 부족, 즉 페이지 캐시 고갈이라는 걸 알고 나니 방향은 분명해졌습니다. 페이지 캐시로 쓸 수 있는 시스템 메모리를 늘려야 했습니다.
CPU 사용률을 보면 간헐적으로 최대 80%까지 튀긴 했지만 평균은 30%대에 머물렀습니다. CPU는 여유가 있고 메모리가 병목인 상황이었던 거죠. 그래서 범용(General Purpose) 인스턴스에서 메모리 최적화(Memory Optimized) 인스턴스로 옮기는 게 자연스러운 선택이었습니다.
| 항목 | As-Is | To-Be | 변화 |
| 인스턴스 타입 | m7g.4xlarge (범용) | r7g.2xlarge (메모리 최적화) | 메모리 중심으로 전환 |
| 노드 수 | 8대 | 12대 | +50% |
| 총 vCPU | 128 | 96 | −25% |
| 총 메모리 | 512 GB | 768 GB | +50% |
| 월 청구 금액 | – | – | 거의 동일 |
[Table 3] 인스턴스 스펙 변경 (As-Is → To-Be) — 범용(m7g) 대형 8대에서 메모리 최적화(r7g) 중형 12대로 전환하여, 월 청구 금액을 유지하면서 총 메모리를 50% 확장
CPU를 줄이는 대신 메모리를 늘리는 전략으로, m7g.4xlarge에서 한 단계 작은 r7g.2xlarge로 사이즈를 낮추되 노드 수를 8대에서 12대로 늘렸습니다. 같은 크기끼리 비교하면 메모리 최적화(r7g)가 범용(m7g)보다 단가가 높지만, 사이즈를 줄여 노드당 비용을 낮추고 대수를 늘리는 방식으로 총 메모리를 512 GB에서 768 GB로 50% 키우면서도 월 청구 금액은 거의 그대로 유지할 수 있었습니다.
개선 결과 (9월 9일(9/9) 인스턴스 개선 반영):
아래 그래프를 보면 9월 9일(9/9) 신규 스펙을 반영한 시점을 기준으로 CPU 스파이크 완화, 검색 처리량 안정화, 검색 레이턴시 개선이 함께 나타납니다.

[Figure 5-1] CPU 사용률 추이 — 신규 스펙 반영 시점 이후 간헐적 스파이크가 크게 줄어듦

[Figure 5-2] 검색 처리량(operations/min) 추이 — 동일 처리량 수준에서 신규 스펙 반영 후에도 안정적으로 유지

[Figure 5-3] 검색 지연 시간(Search Latency) 추이 — 신규 스펙 반영 이후 평균 레이턴시가 낮아지고 변동 폭이 줄어듦
| 지표 | As-Is (9/3~9/5) | To-Be (9/10~9/12) | 변화 |
| CPU (AVG) | 13.8% | 13.7% | 거의 동일 |
| CPU (MAX) | 34.1% | 40.3% | +6.2%p |
| CPU 스파이크 | 80%까지 간헐적 발생 | 대폭 감소 | 노드 분산 효과 |
| Read IOPS (AVG) | 4,450 | 3,010 | -32% |
| Read IOPS (MAX) | 5,130 | 3,410 | -34% |
| Search Latency (AVG) | 18.2 ms | 14.4 ms | -21% |
[Table 4] 인스턴스 스펙 개선 전후 지표 비교 (9/9 반영) — 총 vCPU 감소로 최대 CPU는 소폭 상승했으나, 시스템 메모리 확장으로 Read IOPS(MAX) -34%, 검색 레이턴시 -21% 개선
총 vCPU가 128에서 96으로 줄어 최대 CPU 사용률은 약 6%p 올라갔지만, 정작 기존에 80%까지 튀던 스파이크는 오히려 줄었습니다. 노드 수가 늘면서 샤드가 더 고르게 분산되어, 특정 노드에 부하가 쏠리던 현상이 완화된 것으로 보입니다. 핵심 지표인 IOPS(MAX)는 5,130에서 3,410으로 34% 좋아졌고, 검색 레이턴시도 평균 18.2 ms에서 14.4 ms로 21% 줄었습니다. 시스템 메모리가 늘어난 덕에 OS 페이지 캐시에 여유가 생겼고, stored fields를 읽을 때마다 나던 페이지 캐시 미스가 그만큼 줄어든 결과입니다.
대량 업데이트 후 IOPS 폭발
인스턴스 스펙을 바꾸면서 전반적으로는 안정을 찾았지만, 끝까지 남은 문제가 하나 있었습니다. 매일 새벽 하루치 데이터를 한꺼번에 업데이트하는 배치가 도는데, 이 배치가 끝난 직후마다 IOPS가 폭발적으로 치솟는 현상이 반복됐습니다.
원인은 대량 업데이트가 부른 세그먼트 병합 폭발이었습니다.
Lucene에서 문서 업데이트는 사실 기존 문서를 삭제 마킹(soft delete)하고 새 문서를 새 세그먼트에 쓰는 방식입니다. 하루치 데이터를 한꺼번에 업데이트하면 수백만 건의 새 세그먼트가 쏟아지고, 병합 정책(Merge policy)이 이를 병합하기 시작합니다. 세그먼트 병합은 원본 세그먼트의 모든 파일을 읽어 새 세그먼트로 다시 쓰는 작업이라, 그 자체로 막대한 디스크 I/O를 만듭니다.
게다가 미리캔버스는 _source에 벡터 필드 5개가 들어 있어 stored fields가 크고, 병합할 때 읽고 써야 할 데이터양도 일반 인덱스보다 훨씬 많았습니다. 병합 I/O와 검색 I/O가 같은 EBS 볼륨을 나눠 쓰다 보니, 병합이 몰리는 시간대에는 검색 성능까지 함께 떨어지는 구조였습니다.
이를 풀어보려고 몇 가지를 시도했습니다.
`_forcemerge`와 reindex 시도: 배치가 끝나고 트래픽이 들어오기 전에 강제 병합을 여러 번 돌려봤습니다. 세그먼트 수는 잠깐 줄었지만, 인덱싱 양이 워낙 많다 보니 금세 다시 불어나 같은 문제가 반복됐습니다. reindex도 마찬가지였습니다. 무엇보다 _forcemerge와 reindex 둘 다 그 자체가 무거운 I/O 작업이라, 가뜩이나 부담이 큰 클러스터에서 주기적으로 돌리면 서비스에 직접 영향을 줄 수밖에 없어 당장의 해법으로는 무리였습니다.
`refresh_interval` 조정: 기존 10초에서 1분으로 늘려 새로 만들어지는 세그먼트 수를 줄여보려 했습니다. 이 역시 인덱싱 양이 많다 보니 효과가 미미했습니다.
이쯤 되니 기존 환경에서 짜낼 수 있는 최적화는 거의 바닥이 났고, 좀 더 근본적인 해법이 필요한 시점이 왔습니다.
마무리
여기까지가 미리캔버스가 듀얼 벡터 검색을 도입하고, OpenSearch 2.19 환경에서 메모리와 IOPS 병목에 대응한 과정이었습니다. 쿼리 설계와 인스턴스 스펙 조정으로 상당 부분을 개선했지만, 대량 업데이트 배치가 유발하는 세그먼트 병합 폭발은 2.19 환경에서 끝내 해결하지 못한 과제로 남았습니다.
이 과제를 근본적으로 해결한 OpenSearch 2.19에서 3.3으로의 메이저 버전 업그레이드 과정은 다음 글에서 이어집니다.
