[Milvus DB] Excel 데이터 임베딩 최적화 과정 4단계
1. 서론
대규모 Excel 데이터를 벡터화하여 검색 가능한 형태로 관리하려면, 임베딩 후 벡터 DB(Milvus)에 저장하는 과정이 필수적이다.
Milvus는 대규모 벡터 데이터를 빠르게 검색할 수 있는 오픈소스 벡터 데이터베이스로, 문서 검색, 추천 시스템, AI 응답 최적화 등 다양한 활용 사례에서 쓰인다.
하지만 대량 데이터를 어떻게 저장하느냐에 따라 속도와 안정성이 크게 달라진다.
이번 작업에서는 고객사의 내부 사전을 학습시켜야 했는데, 엑셀 파일 내부에 3천개가 넘는 행이 있었다. 샘플로 만들어둔 한영사전 Excel 시트의 행 단위 데이터를 Milvus에 임베딩 및 저장하는 과정을 최적화하면서
- 한 번에 모두 저장하는 방식
- 한 행씩 저장하는 방식
- 일정 개수 단위로 배치를 저장하는 방식
- 비동기(async)로 배치를 저장하는 방식
순서대로 이렇게 네 가지 접근을 시도했고, 그 과정에서 얻은 인사이트를 정리해보려고 한다.
2. 본론
1) 한꺼번에 전부 저장
가장 단순한 방법은 Excel 파싱 후 모든 행을 리스트로 변환하고 한 번에 Milvus에 insert하는 방식이었다.
docs = convert_excel_terms(file_path)
milvus_db.insert_documents(docs) # 한 번에 DB에 삽입
이 방식은 네트워크 호출 1회로 끝나기 때문에 굉장히 빠르다.
Milvus도 내부적으로 벡터 인덱싱을 배치 단위로 처리하므로 성능 면에서는 가장 효율적이다.
하지만 단점이 있다:
- 데이터가 전부 들어가기 전까지는 ATTU(관리 툴)에서 확인 불가
- 대량 삽입 중 일부 실패가 발생하면 어느 행이 실패했는지 알 수 없음
결과적으로 전체 성공 또는 전체 실패의 형태라 문제가 생길 경우 부분 실패 복구가 어렵다는 문제가 있었다.
2) 한 행씩 저장
부분 실패를 허용하고, 실시간으로 저장 상태를 확인하고 싶어서 for문으로 한 행씩 insert를 시도했다.
for doc in docs:
milvus_db.insert_documents([doc])
이 방식은 ATTU(GUI기반 Milvus 오픈소스 관리 툴)에서 바로 확인 가능하고, 실패한 행만 스킵할 수도 있었다.
그러나 속도가 심각하게 느려졌다.
왜냐하면 Milvus insert는 네트워크 I/O + 벡터 인덱싱 작업을 수반하기 때문에 4,000개 행이라면 4,000번 네트워크 호출이 일어나기 때문이다.
실제로 샘플로 만들어둔 한영 사전 엑셀 파일로 테스트했을 때 한 행당 저장에 수백 밀리초 이상이 걸려 행이 100개도 안됐지만 전체 시간이 너무 오래 걸렸다.
3) 배치 저장으로 타협
그래서 속도와 안정성을 모두 확보하기 위해 50개 단위 배치 저장으로 최적화했다.
batch_size = 50
buffer = []
for doc in docs:
buffer.append(doc)
if len(buffer) >= batch_size:
milvus_db.insert_documents(buffer) # 50개 단위 삽입
buffer.clear()
# 마지막에 남은 데이터 처리
if buffer:
milvus_db.insert_documents(buffer)
이 방식으로 기존의 문제를 크게 해결했다.
- 네트워크 호출 횟수를
총행수 / batch_size로 줄여서 속도를 크게 개선 - 여전히 배치 단위로 부분 확인 가능
- 실패 시 어느 배치가 실패했는지 로그로 확인 가능
실제 실행 후에 확인해본 로그는 다음과 같았다.
[excel_terms_embedder_process Start] ######### 11:07:20 #########
(...)
[INFO] 현재까지 50/87개 처리 완료 (성공 50, 실패 0)
(...)
[excel_terms_embedder_process End] ######### 11:07:35 #########
[excel_terms_embedder_process Total time] ######### 14.29 seconds #########
샘플 데이터의 전체 87개 행을 저장하는 데 14초대로 완료되어 한 행씩 저장할 때보다 훨씬 빠르고,
한 번에 저장할 때보다 안정성도 확보되었다.
4) 비동기(async) 배치 저장으로 추가 최적화
이후 시니어 개발자가 Milvus 저장 로직을 개선하면서 sync/async insert 기능이 추가되었다.
이제 insert_documents 메서드는 sync 옵션을 받을 수 있게 되었고 비동기(async) 모드를 활용하면 insert 작업을 별도의 스레드 풀에서 실행하도록 되어 있다.
import asyncio
from concurrent.futures import ThreadPoolExecutor
class MilvusSearchEngine:
def __init__(self, collection_name: str):
self.collection_name = collection_name
self.collection = self._connect_collection(collection_name)
# 비동기 insert 처리를 위한 ThreadPoolExecutor (동시에 최대 10개 처리)
self.executor = ThreadPoolExecutor(max_workers=10)
def _connect_collection(self, collection_name):
# Milvus 컬렉션 연결 (실제 연결 로직 생략)
...
return collection
async def insert_document_async(self, documents):
"""
비동기 document 삽입
→ Milvus insert 작업을 별도의 스레드에서 실행
"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
self.executor, # 백그라운드에서 실행
self.collection.insert, # Milvus insert 호출
documents
)
def insert_documents(self, documents, sync: bool = True):
"""
Milvus에 문서를 삽입 (sync=True → 동기 / sync=False → 비동기)
"""
if sync:
# 동기 모드 → insert 후 flush까지 완료될 때까지 대기
self.collection.insert(documents)
self.collection.flush()
print(f"[SYNC] {len(documents)} docs inserted")
else:
# 비동기 모드 → insert 요청만 던지고 즉시 반환
asyncio.create_task(self.insert_document_async(documents))
print(f"[ASYNC] {len(documents)} docs insert requested")
비동기 모드에서는 asyncio + ThreadPoolExecutor를 이용해 백그라운드에서 insert가 처리된다.
즉, 배치 저장 로직을 이렇게 변경할 수 있었다:
if len(buffer) >= batch_size:
# 비동기 처리로 insert 요청만 던지고 다음 로직으로 넘어감
milvus_db.insert_documents(buffer, sync=False) # sync가 false이므로 비동기
buffer.clear()
이렇게 하면
- 메인 로직이 insert 완료를 기다리지 않고 바로 다음 배치로 넘어가므로 동일한 배치를 처리하는데 14.29 → 2.96초로 I/O 대기 시간을 대폭 줄일 수 있다.
[excel_terms_embedder_process Start] ######### 16:14:33 #########
(...)
총 50개 쿼리 처리 완료
Dense embeddings: 50개
Sparse embeddings: 50개
ColBERT embeddings: 0개
(...)
[excel_terms_embedder_process End] ######### 16:14:36 #########
[excel_terms_embedder_process Total time] ######### 2.96 seconds #########
INFO: 127.0.0.1:52049 - "POST /api-rag/smcor/excel-terms-embedder HTTP/1.1" 200 OK
- Milvus insert는 백그라운드에서 비동기 처리되며, flush는 별도로 한 번만 수행할 수 있다.
결과:
- 배치 + 비동기 insert 조합으로 DB 삽입 지연이 사실상 사라졌고,
- 로그 최적화로 CPU/콘솔 I/O 오버헤드도 줄어들어 전체 파이프라인이 훨씬 가볍고 빠르게 동작했다.
3. 결론
Milvus는 대규모 벡터 데이터 검색에 특화된 DB지만, insert 방식에 따라 성능 체감이 크게 달라진다.
- 한 번에 insert: 가장 빠르지만, 중간 확인 불가
- 한 행씩 insert: 실시간 확인 가능하지만, 네트워크 호출이 너무 많아 느림
- 배치 insert: 속도와 안정성을 모두 고려한 현실적인 선택
- 비동기 배치 insert: 배치 insert보다도 더 빠르게 처리 가능하며 CPU/네트워크 자원 활용 최적
결국 현재는 50~100개 단위로 비동기 배치 insert가 가장 이상적인 절충안이었다.
이번 경험으로 얻은 핵심은
- Milvus와 같은 벡터 DB도 일반 DB와 마찬가지로 네트워크/인덱싱 비용이 크다
⇒ 따라서 <적절한 배치 전략과 비동기 insert 활용, 그리고 불필요한 로그 최적화>가 성능 개선의 핵심이라는 점이다.
향후에는
- 비동기 insert 완료 후 상태 모니터링
- 실패한 배치만 자동 재시도
- Milvus 인덱스 빌드 시점 최적화
이런 추가 개선도 고려할 수 있을 것 같다.