검색 시스템은 왜 이렇게 복잡해졌나?
최근 AI 연구 및 서비스 개발에서 '검색'의 역할이 점점 중요해지고 있습니다. 특히 논문 검색처럼 정확도가 생명인 도메인에서는 단순 키워드 매칭만으로는 부족합니다. 사용자가 "작은 언어 모델로 코드 생성을 어떻게 하지?"라고 검색했을 때, 논문 제목에 정확히 그 단어들이 없어도 관련성을 찾아내야 하기 때문이죠.
Papers with Code 팀은 이 문제를 해결하기 위해 키워드 검색과 벡터 검색을 결합한 하이브리드 아키텍처를 선택했습니다. 이 글에서는 그들이 어떻게 110,000개가 넘는 논문에 대한 임베딩을 생성하고, 실시간 검색 서비스를 운영하는지 그 설계 철학을 분석해 보겠습니다.
이 글이 주목할 점
단순히 기술 스택을 나열하는 것이 아니라, "배치 작업"과 "실시간 요청"이라는 상반된 성격의 워크로드를 어떻게 분리하고 연결했는지가 핵심입니다. 이는 단순한 검색 시스템을 넘어, 머신러닝 모델을 실제 프로덕션에 통합하려는 모든 개발자에게 유효한 설계 패턴을 제시합니다.
시스템 설계의 핵심: 배치와 실시간의 분리
이 아키텍처의 가장 중요한 결정은 임베딩 생성 작업을 두 가지 경로로 분리한 것입니다.
- 오프라인 배치(Offline Batch): 전체 논문 코퍼스를 대상으로 하는 대규모 임베딩 생성. 처리량(Throughput)이 중요하며, GPU를 한정된 시간 동안만 사용합니다.
- 온라인 서비스(Online Service): 사용자 검색어를 실시간으로 임베딩하는 작업. 지연 시간(Latency)이 중요하며, 항상 가용해야 합니다.
이 두 가지를 하나의 시스템으로 묶으려고 했다면, 비용은 비용대로 들고 성능은 성능대로 나오지 않는 최악의 상황이 발생했을 것입니다. 이 글에서는 이 분리를 위해 Hugging Face의 세 가지 서비스를 어떻게 조합했는지 자세히 설명합니다.
![]()
구성 요소 분석: Jobs, Buckets, Inference Endpoints
각 서비스의 역할을 도식화하면 아래와 같습니다.
- Hugging Face Jobs: GPU 인스턴스를 생성하여 대규모 배치 작업을 실행합니다. (예: NVIDIA L4 GPU)
- Hugging Face Storage Buckets: Jobs의 결과물과 데이터 스냅샷 등 중간 산출물을 저장하는 저장소입니다.
- Hugging Face Inference Endpoints: 실시간 검색어 임베딩 및 소량의 증분 업데이트를 처리하는 상시 대기 서버입니다.
# 개념 증명을 위한 간단한 코드 구조 (실제 구현은 공식 문서 참조)
# 1. 배치 작업 (Jobs): DB 스냅샷을 읽어와 전체 논문 임베딩 생성
def run_embedding_job(snapshot_path: str, output_path: str):
"""전체 코퍼스에 대한 임베딩을 생성하고 저장하는 가상의 배치 작업"""
papers = load_papers_from_snapshot(snapshot_path)
embeddings = []
for batch in chunk(papers, batch_size=64):
vectors = embedding_model.encode_document(batch)
embeddings.extend(vectors)
save_embeddings_to_parquet(output_path, embeddings)
# 2. 온라인 검색 (Inference Endpoints): 사용자 쿼리를 벡터화하고 DB에서 검색
@app.get("/search")
def search_papers(query: str):
"""실시간 검색 API: 쿼리를 임베딩하고 PostgreSQL에서 벡터 검색 수행"""
query_vector = embedding_model.encode_query(query)
# pgvector를 사용한 유사도 검색 (예: cosine distance)
results = db.execute(
"""
SELECT paper_id, embedding <=> :query_vector AS distance
FROM paper_embeddings
ORDER BY distance
LIMIT 50
""",
{"query_vector": query_vector}
)
return results
왜 이런 구조가 효율적인가?
| 특성 | 배치 작업 (Jobs) | 실시간 서비스 (Inference Endpoints) |
|---|---|---|
| 워크로드 | 대량의 데이터 처리, 높은 처리량 | 소량의 요청, 낮은 지연 시간 |
| 리소스 | 필요할 때만 GPU 인스턴스 사용 | 상시 대기 또는 오토스케일링 |
| 비용 최적화 | Scale-to-zero 가능 | 콜드 스타트를 고려한 설계 필요 |
| 실패 처리 | 재시도 및 재개(Resume) 용이 | 빠른 타임아웃 및 폴백(Fallback) 필수 |
이 표에서 보듯, 두 작업의 성격이 완전히 다르기 때문에 인프라를 분리하는 것이 합리적입니다. 특히 Storage Buckets가 두 시스템 간의 '계약(Contract)' 역할을 한다는 점이 인상적입니다. 작업의 결과물이 체크섬(Checksum)과 매니페스트(Manifest)와 함께 저장되어, 이후 검증 및 롤백이 용이하도록 설계되었습니다.
하이브리드 검색의 힘: RRF와 장애 대비
이 시스템의 검색 품질의 핵심은 상호 랭크 융합(Reciprocal Rank Fusion, RRF) 알고리즘입니다. 단순히 키워드 검색과 벡터 검색의 결과를 합치는 것이 아니라, 각각의 **순위(Rank)**를 기반으로 점수를 계산하여 더 지능적으로 결과를 결합합니다.
RRF의 장점
- 점수 체계 무관: 키워드 검색의 BM25 점수와 벡터 검색의 코사인 유사도는 그 스케일이 달라 직접 비교가 불가능합니다. RRF는 순위만 사용하므로 이 문제를 해결합니다.
- 강건성(Robustness): 두 검색 시스템 중 하나가 다소 부정확하더라도, 다른 하나가 결과를 보완할 수 있습니다.
실제 운영 환경의 설계 철학
이 아키텍처에서 가장 중요한 부분은 Inference Endpoints가 실패했을 때의 동작입니다. 시스템은 성능 저하를 '예외적인 상황'으로 보지 않고, 기본적인 동작 환경의 일부로 간주합니다.
- 짧은 타임아웃: 쿼리 임베딩에 1초의 타임아웃을 설정.
- 서킷 브레이커: 연속 실패 시 즉시 벡터 검색 경로를 차단.
- 자동 폴백: 벡터 검색이 불가능하면 항상 키워드 검색 결과만 반환.
이러한 설계는 검색 시스템이 단일 장애점(Single Point of Failure)에 의존하지 않도록 합니다. 이는 '가용성'과 '완벽한 정확도' 사이의 균형을 찾는 과정에서 나온 결정입니다.

비판적 검토: 이 아키텍처의 함정과 주의사항
이 설계는 매우 훌륭하지만, 모든 상황에 완벽한 해법은 아닙니다. 실제로 도입을 고려한다면 다음의 한계점을 반드시 인지해야 합니다.
1. 인프라 복잡도 증가
단순히 PostgreSQL 하나로 검색을 구현하는 것에 비해, Jobs, Buckets, 별도의 Endpoint를 운영하는 것은 분명히 인프라 복잡도를 증가시킵니다. 소규모 서비스나 초기 스타트업에는 과한 엔지니어링이 될 수 있습니다. 글에서도 언급하듯, 키워드 검색만으로 충분한지 먼저 검증하는 것이 더 합리적인 접근법입니다.
2. 임베딩 모델에 대한 높은 의존성
시스템의 성능은 선택한 임베딩 모델(Qwen3-Embedding-0.6B)의 품질에 크게 좌우됩니다. 모델이 업데이트되거나 교체될 때마다 전체 코퍼스에 대한 재임베딩이 필요할 수 있으며, 이는 상당한 시간과 비용이 소요되는 작업입니다. 임베딩 버전 관리의 중요성을 이 사례에서 다시 한번 확인할 수 있습니다.
3. 한국어 지원의 문제 (KO 한정)
이 시스템은 기본적으로 영어 논문을 대상으로 합니다. 한국어 검색에 이 구조를 그대로 적용하려 한다면, 한국어 특화 임베딩 모델의 선택이 매우 중요해집니다. 한국어는 교착어적 특성 때문에 토큰화 방식에 따라 검색 품질 차이가 크게 발생할 수 있습니다. 또한, 한국어 특유의 띄어쓰기 오류나 신조어에 강건한 형태소 분석기가 필요한지도 추가로 고려해야 합니다.
결론: 검색을 하나의 '제품'으로 바라보기
이번 사례가 주는 가장 큰 교훈은 검색을 단순한 기능이 아닌, 하나의 '제품'으로 바라봐야 한다는 점입니다. 단순히 기술적인 정확도만 높이는 것이 아니라, 비용, 속도, 가용성이라는 여러 제약 조건 사이에서 최적의 균형점을 찾아내는 것이 핵심 엔지니어링 역량입니다.
배치와 실시간의 분리, 명시적인 저장소 계층, 그리고 장애를 가정한 설계는 대규모 AI 시스템을 운영할 때 반드시 고려해야 할 훌륭한 패턴입니다. 이 구조를 통해 우리는 비용은 낮추면서도 성능과 정확도를 동시에 확보할 수 있는 방법을 배울 수 있습니다.
이러한 시스템 설계는 결국 '개인화'나 '실험'과 같은 복잡한 도메인에서도 유사한 고민이 필요합니다. 다양한 기술 스택을 어떻게 분리하고 연결할지에 대한 인사이트가 필요하다면, 개인화와 실험 시스템의 분리 전략에 대한 글을 참고해 보세요. 또한, 대규모 시스템 운영의 또 다른 측면인 비용 최적화와 자동화에 관심이 있다면 Generali Malaysia의 EKS 운영 최적화 사례도 함께 읽어보시길 추천합니다.
다음 단계 학습 방향
- 임베딩 모델 심화: MTEB 리더보드를 탐색하며 다양한 태스크에 적합한 모델을 선택하는 기준을 학습해 보세요.
- 벡터 DB 비교: pgvector 외에도 Pinecone, Milvus, Weaviate 등 다양한 벡터 데이터베이스의 특성을 비교해 보세요.
- RRF 및 랭킹 알고리즘: Learning to Rank(LTR) 알고리즘과 RRF의 차이점 및 적용 시나리오를 연구해 보세요.

실무 적용을 위한 요약 체크리스트
- 워크로드 특성 분석: 내 서비스의 검색 트래픽과 데이터 규모를 기준으로 배치/실시간 분리가 필요한지 검토.
- 임베딩 계약 정의: 모델명, 리비전, 차원 수, 정규화 방법 등을 코드와 설정 파일에 명시적으로 관리.
- 장애 대비 설계: 벡터 검색 엔진이 죽었을 때 서비스가 완전히 멈추지 않도록 폴백 로직을 구현.
- 비용-품질 트레이드오프: Matryoshka Representation Learning과 같은 기술을 활용해 벡터 차원을 줄여 비용을 절감하는 방법을 고려.