
"Sage" 확장하기: 네팔 최초의 자체 개발 표절 방지 시스템 구축
요약
네팔 Softwarica College에서 개발한 Sage 표절 방지 시스템의 성능 최적화 과정을 다룹니다. Qdrant 벡터 엔진을 활용하여 4,880만 개의 문장 임베딩을 처리하며, 양자화 기술을 통해 속도와 메모리 효율을 극대화했습니다.
핵심 포인트
- Qdrant 엔진 도입 및 샤딩을 통해 보고서 생성 시간을 13.4배 단축
- 4비트 양자화 인덱스 적용으로 RAM 사용량을 40-60GB에서 11GB로 절감
- 초당 약 1,300개의 쿼리 처리 및 10,000개 동시 쿼리 시 안정적 성능 유지
- 양자화 적용 후에도 전체 정밀도 대비 99%의 높은 판정 일치율 기록
날짜: 2026-07-14
프로젝트: Sage 표절 방지 시스템, 학술적 무결성(academic integrity)을 위해 Softwarica College of IT & E-Commerce에서 구축된 네팔 최초의 자체 개발 표절 방지 프로젝트.
시스템: 약 4,880만 개의 문장 임베딩 코퍼스(embedding corpus)에 대한 표절 유사도 검색
벡터 엔진 (Vector engine): Qdrant 1.18.2 (384차원 임베딩, 코사인 거리 (cosine distance), 8개 샤드 (shards), 52개 세그먼트 (segments))
요약 (TL;DR)
1년이 넘는 기간 동안, 보고서 생성 시간은 62.5초에서 4.67초로 단축되었으며(13.4배 빨라짐), RAM 사용량은 40~60GB에서 11GB로 감소했습니다. 결정적인 릴리스(v4)에서는 4비트 양자화된 인덱스 (4-bit-quantized index)와 내비게이션 그래프 (navigation graph)를 동시에 RAM에 배치하여, 속도와 경량화를 동시에 달성했습니다. 3단계 부하 테스트 (load test) 결과, 초당 약 1,300개의 쿼리 (queries/second)라는 한계치를 발견했으며, 10,000개의 동시 쿼리 (concurrent queries) 상황에서도 오류 없이 성능이 점진적으로 저하(degrading gracefully)되었고, 100개 문장 제출 시 실제 사용자 지연 시간 (latency)은 약 90ms였습니다. 4비트 압축의 비용은 거의 없습니다. 전체 정밀도 (full precision) 대비 판정 일치율 99%, 점수 오차 0.3%를 기록했습니다 (§4).
1. 목표
운영 환경의 유사도 검색 백엔드(similarity-search backend)에 관한 두 가지 질문:
- 얼마나 강하게 밀어붙일 수 있는가? 처리량(throughput)의 한계를 찾고, 부하 상황에서 실패하는 대신 성능이 점진적으로 저하되는지 확인한다.
- 실제 사용자는 얼마나 기다리는가? 경합(contention)이 없는 상태에서 한 번의 제출에 대한 검색 지연 시간 (search latency)을 측정한다.
이 보고서는 또한 시스템이 지난 1년 동안 현재의 성능에 도달한 과정을 기록하고 있으므로, 스트레스 테스트 (stress-test) 수치를 해당 궤적과 비교하여 읽을 수 있습니다.
2. 1년간의 최적화 진화 과정
508개의 문장 임베딩 (4,495개 단어)으로 구성된 고정된 참조 제출물을 기준으로 벤치마크를 수행하여, 엔드 투 엔드 (end-to-end) 표절 보고서 시간을 측정했습니다.
| 버전 | 날짜 | 주요 변경 사항 | RAM | 보고 시간 | v2 대비 속도 향상 |
|---|---|---|---|---|---|
| v2 | 2025년 7월 28일 | Milvus에서 Qdrant로 마이그레이션 | 40 GB | 62.53 s | 1.0× |
| ... |
- v2에서 v3로: 8개의 샤드 (shards)를 통해 쿼리가 병렬로 분산 (fan out)되어 보고 시간을 약 40% 단축했으나, 더 많은 인덱스를 메모리에 상주 (resident) 시킴으로써 RAM 사용량이 60 GB로 증가하여 확장성 측면에서 잘못된 방향으로 진행되었습니다.
- v3에서 v4로: 벡터를 4비트 (4 bits)로 압축하고 벡터와 탐색 그래프 (navigation graph)를 모두 RAM에 고정 (pinning)함으로써, 7.5배의 속도 향상과 82%의 RAM 감소 (60 GB에서 11 GB로)를 동시에 달성했습니다. 아키텍처는 §3에 기술되어 있습니다.
- v4에서 v5로: 검색 (search)을 병렬화하여 RAM 비용 증가 없이 마지막 7%의 시간을 제거했습니다. 이 스트레스 테스트 (stress test)는 v5에서 병렬화된 검색 컴포넌트를 실행합니다. 이는 전체 엔드 투 엔드 (end-to-end) 보고 시간이 아닌 해당 컴포넌트의 처리량 (throughput)과 지연 시간 (latency)을 측정합니다 (범위는 §5에 기술됨).
이 궤적을 통제된 절제 연구 (ablation study)가 아닌, 실제 환경에서의 개선 기록으로 읽으십시오. (1) v4는 4비트 양자화 (4-bit quantization)와 그래프-인-RAM (graph-in-RAM)이라는 두 가지를 동시에 변경했으므로, 단일 릴리스의 이득을 하나의 변경 사항 덕분이라고 단정 지을 수 없습니다. (2) 1년 동안 코퍼스 (corpus)가 성장했기 때문에, 13.4배라는 수치는 최적화와 변화하는 데이터 베이스라인 (baseline)이 혼합된 결과입니다.
3. 현재 아키텍처: TurboQuant와 HNSW-in-RAM의 결합
전정밀도 (full precision) 상태에서 벡터는 RAM에 담기에 너무 큽니다. v4/v5 설계는 세 개의 메모리 계층 (memory tiers)으로 구성된 두 가지 최적화를 동시에 실행함으로써 어쨌든 검색을 빠르게 만듭니다. (TurboQuant는 Qdrant 1.18에 탑재된 회전 기반의 훈련이 필요 없는 (training-free) 양자화 방식입니다. 4비트는 스칼라 양자화 (scalar quantization) 대비 1~2 pp의 재현율 (recall) 차이로 8배의 압축을 제공합니다.)
- Tier 1, 탐색 그래프 (navigation graph), RAM 내 위치. 계층적 근접 그래프 (노드당 32개의 엣지, 512-이웃 구축 패스로 생성)가 어떤 벡터를 조사할지 결정합니다. 후보 선택 (candidate selection)은 디스크 액세스 없이 순수하게 메모리 내에서만 이루어지는 작업입니다.
- Tier 2, 4비트 벡터 (4-bit vectors), RAM 내 위치. 모든 벡터는 차원당 32비트에서 4비트로 압축되어 RAM에 고정(pinned)되며, 탐색 과정 중 후보들의 점수를 매기는 데 사용됩니다. 이는 핫 패스 (hot path) 상에서 디스크를 사용하지 않는 빠른 근사 거리 (approximate distances) 계산입니다.
- Tier 3, 전체 정밀도 벡터 (full-precision vectors), 디스크 상 위치. 정확한 32비트 벡터는 소수의 상위 후보들에 대해 점수를 다시 매길 (rescore) 때만 읽어 들여지며, 이를 통해 RAM 비용을 지불하지 않고도 전체 정확도를 복원합니다.
4-bit: 48.8M × 384 × 4 bits ≈ 9.4 GB → ~10 GB resident
f32: 48.8M × 384 × 4 bytes ≈ 75 GB (84.8 GB measured on disk, incl. overhead)
─────────────
...
왜 하나가 아닌 둘 다인가
| TurboQuant만 사용 시 | HNSW-in-RAM만 사용 시 | 둘 다 사용 (현재) | |
|---|---|---|---|
| 후보 선택 | 디스크 상의 그래프, 느림 | RAM, 빠름 | RAM, 빠름 |
| ... |
후보 선택 및 후보 점수 매기기(candidate scoring)를 포함한 전체 핫 루프 (hot loop)는 약 11.5 GB 내의 RAM에서 실행되는 반면, 부피가 큰 정확한 벡터들은 디스크에 머물며 최종 재점수화 (rescore) 단계에서만 접근됩니다. 이것이 v4를 v3보다 동시에 7.5배 더 빠르고 5.5배 더 가볍게 만들 수 있었던 비결입니다. 만약 어느 한 계층이라도 디스크에 존재했다면, 핫 루프가 매 쿼리마다 디스크에 접근하게 되어 지연 시간 (latency)이 훨씬 더 나빠졌을 것입니다.
실시간 메모리 점유율 (Live memory footprint)
| 디스크 (Disk) | RAM | |
|---|---|---|
| 벡터 (Vectors) | 84.8 GB (전체 정밀도) | 10.0 GB (4-bit) |
| ... |
4비트 근사치의 정확도는 §4에서 별도로 검증되었습니다 (99% 판결 일치, 0.3% 점수 오차). 따라서 0.80 임계값 (threshold)에서는 압축 비용이 사실상 무료입니다.
4. 검색 품질: 4비트 양자화 (quantization)가 정확도를 저하시키는가?
압축된 검색은 올바른 매칭 결과를 반환할 때만 유용합니다. 4비트 검색 품질 (retrieval quality)은 f32 근사 정밀도 (near-exact) 그라운드 트루스 (ground truth)를 기준으로 측정되었습니다. 이는 라이브 컬렉션(collection) 상에서 읽기 전용으로, 깊은 탐색 폭(ef 256, 프로덕션의 ef 64의 4배)에서 양자화 (quantization)를 무시하고 검색한 동일한 In-RAM 그래프를 사용했습니다. 100개의 샘플링된 실제 쿼리(query)를 대상으로 top_k를 최대 10까지 설정했습니다.
올바른 지표. 표절 탐지 (plagiarism detection)는 정확히 어떤 코퍼스 (corpus) 지점이 반환되는지에 의존하지 않습니다. 코퍼스는 거의 중복된 문장들로 밀집되어 있어, 많은 지점들이 거의 동일한 유사도 (similarity)를 가집니다. 중요한 것은 유사도 점수가 보존되는지, 그리고 0.80 코사인 임계값 (cosine threshold)을 넘느냐 하는 것입니다. 코사인 유사도 0.998과 0.999는 동일한 판결을 내리며, 어느 쪽이 승리하는지는 무관합니다.
| 지표 (Metric) | 값 (Value) | 의미 (Meaning) |
|---|---|---|
| 임계값 일치도 (0.80에서 동일한 플래그) (Threshold agreement) | 0.99 | 쿼리의 99%에서 동일한 표절 판결 |
| ... |
엄격한 ID 재현율 (Strict ID recall)은 여기서 오해의 소지가 있습니다. recall@1 = 0.52는 낮아 보이지만, 이는 거의 중복된 데이터로 인한 측정상의 인위적 현상 (artifact)입니다. 실제 이웃 (neighbour)이 0.9991이고 차점자가 0.9989일 때, 4비트 해상도 (resolution)로는 이 둘을 구분할 수 없으므로, 동일하게 유사한 지점을 반환하면서도 "틀린" ID를 반환하게 됩니다. 보고를 결정짓는 두 가지 핵심 지표인 99% 임계값 일치도와 0.3% 점수 오차 (score error)가 실제로 중요한 지표입니다.
결론: 이 워크로드(workload)에서 4비트 압축은 사실상 비용이 들지 않습니다. v4의 속도 및 메모리 이점은 0.80 임계값에서 의미 있는 정확도 저하를 일으키지 않습니다.
범위 (Scope). 프로덕션의 recommend-by-id 작업(동일한 인덱스, 동일한 양자화)의 대리 지표로서 벡터 검색 top-1 일치도를 통해 측정되었습니다. 그라운드 트루스는 근사 정밀도(deep-ef 그래프, 완전한 전체 스캔은 아님)이므로 일치도를 약간 과장할 수 있으나, 99% 및 0.3% 수준에서는 그 차이가 의사결정에 영향을 미칠 정도는 아닙니다. 재현율 (Recall)은 0.80 이외의 제출 유형이나 임계값에 대해 재측정되지 않았습니다.
5. 방법론 (Methodology)
범위 (Scope). 이 테스트는 전체 보고서 생성 과정(파일 다운로드 및 변환, 문장 분할, 임베딩 (Embedding), 삽입, AI 텍스트 탐지 실행, 역검사 실행 포함)이 아닌, 벡터 검색 (Vector-search) 작업만을 분리하여 측정합니다. 검색은 총 4.67초 (§7) 중 약 0.39초를 차지하므로, 이 수치들은 전체 엔드 투 엔드 (End-to-end) 시간의 10% 미만 영역, 즉 전체 파이프라인이 아닌 검색 계층 (Search tier)의 용량을 제한적으로 나타냅니다.
부하 테스트 (Load test)는 실제 운영 환경의 검색 작업인, 배치 (Batched) 형태의 "이 문장들과 가장 유사한 문장들을 찾아라"라는 요청을 수행합니다. 각 요청은 실제 코퍼스 (Corpus)에서 추출한 실제 문장 식별자 (Identifiers)를 포함하며, 제출물이 점수화되는 방식과 동일하게 문장당 단 하나의 가장 가까운 일치 항목을 요청합니다.
테스트 프레임워크 (Harness):
- 실제 문장 식별자를 재사용 풀 (Reuse pool)로 샘플링합니다. 추천 방식의 쿼리 (Query)는 합성 벡터 (Synthetic vectors)가 아닌 기존 코퍼스의 데이터 포인트가 필요합니다.
- 설정 가능한 배치 크기를 포함하는 배치 검색 요청을 발행합니다. 하나의 요청은 하나의 제출물 형태의 점수화 호출과 동일합니다.
- 설정 가능한 워커 (Worker) 수에 따라 요청을 동시에 실행합니다.
- 요청당 지연 시간 (Latency)을 기록하고, 처리량 (Throughput)을 초당 요청 수 (Requests/second)와 초당 쿼리 수 (Queries/second, 요청 수 × 배치 크기)의 두 가지 방식으로 보고하며, p50/p95/p99/max 값을 산출합니다.
| # | 워커 (Workers) | 배치 (문장/요청) | 총 요청 수 | 의도 (Intent) |
|---|---|---|---|---|
| 1 | 1 | 100 | 20 | 실제 단일 제출 지연 시간, 경합 없음 |
| ... |
한계점 (Limitations). 각 설정은 반복이나 웜업 (Warm-up) 없이 한 번씩만 실행되었으므로, 콜드 캐시 (Cold-cache) 대 웜 캐시 (Warm-cache) 상태가 통제되지 않았습니다. 따라서 단일 샘플의 p99/max 값은 정밀한 수치가 아닌 지표로만 해석해야 합니다. 재현율 (Recall)은 측정되지 않았습니다 (처리량과 지연 시간만 측정). 수치는 테스트 날짜의 호스트 및 코퍼스 상태를 반영합니다. 의사결정 수준의 수치를 얻으려면 각 설정을 웜업 단계를 포함하여 3회 반복 실행하고 중앙값 (Medians)을 보고해야 합니다.
6. 결과 (Results)
| Workers | Batch | Queries/s | Requests/s | p50/req | p95 | p99 | max |
|---|---|---|---|---|---|---|---|
| 1 | 100 | 1055 | 10.6 | 90 ms | 200 ms | 200 ms | 200 ms |
| ... | |||||||
| (run 3)의 10,000개 동시 쿼리(concurrent queries)를 포함하여 모든 실행에서 오류가 발생하지 않았습니다. 컬렉션(Collection)은 전체 과정 동안 건강한 상태를 유지했습니다. |
처리량(Throughput)의 상한선은 초당 약 1,300 쿼리(queries/second)입니다. 워커(Workers)와 배치(Batch)가 23배 변동하는 동안에도 처리량은 일정하게 유지(12951339)되며, 이는 서버 측의 명확한 용량 한계(capacity wall)를 나타냅니다. 쿼리당 비용은 1/1300로, 약 0.77 ms입니다.
7. 분석 (Analysis)
포화 제한 (Saturation-bound). 상한선을 넘어서면 처리량은 일정하게 유지되는 반면, 지연 시간(Latency)은 동시성(Concurrency)에 따라 선형적으로 증가하는데, 이는 포화된 큐(Saturated queue)의 동작 방식입니다. 리틀의 법칙 (Little's Law: 처리량 ≈ 동시성 ÷ 평균 지연 시간)은 모든 실행에서 성립합니다:
| Run | Check | Predicted req/s | Measured |
|---|---|---|---|
| 55 × 32 | 55 ÷ 1.298 s | 42.4 | 40.5 ✓ |
| 100 × 100 | 100 ÷ 6.926 s | 14.4 | 13.4 ✓ |
이 일치 결과는 병목 현상(Bottleneck)이 클라이언트나 네트워크가 아닌 서버임을 확인시켜 줍니다. 상한선을 초과하여 워커를 추가하는 것은 처리량을 높이지 못하며, 오직 큐(Queue)만 늘릴 뿐입니다.
지연 시간은 제출 크기(Submission size)에 의해 결정되며, 그다음은 백로그(Backlog)에 의해 결정됩니다. 고정된 0.77 ms/query 조건에서, 제출 지연 시간(Submission latency) = (문장 수 × 0.77 ms) + 큐 대기 시간(Queue wait)입니다.
- 100개 문장, 경합(Contention) 없음: 77 ms + 오버헤드(Overhead), 측정값 약 90 ms ✓.
- 508개 문장 참조: 순수 검색(Pure search)에 약 0.39 s 소요, 이는 4.67 s 보고서의 작은 부분입니다.
- Run 3의 7.4 s p50은 "한 번의 제출에 7 s가 걸린다"는 의미가 아닙니다. 이는 100개의 100문장 제출(10,000개 쿼리)이 초당 1,300개 처리 가능한 파이프(Pipe)를 통해 빠져나가는 상황이므로, 배치를 비우는 데 약 10,000 ÷ 1,300 ≈ 7.5 s가 소요되는 것입니다. 이는 실제 시나리오가 아닌, 스스로 초래한 과부하(Self-inflicted overload)입니다.
성능 저하(Degradation)는 안전합니다. 1개에서 10,000개의 동시 쿼리(concurrent queries)가 발생해도 오류, 타임아웃 또는 충돌(crash)이 발생하지 않습니다. 시스템은 느려질 뿐 결코 실패하지 않습니다. 부하 상황에서도 RAM 사용량은 일정하게 유지됩니다. 핫 티어(hot tiers)는 크기가 고정되어 있고 고정(pinned)되어 있으므로, 동시성(concurrency)은 메모리가 아닌 CPU, 검색 스레드(search threads) 및 디스크 재점수화(disk-rescore) 경로를 소모합니다. 이것이 v5의 병렬화(parallelization)가 11 GB의 메모리 점유율(footprint)을 늘리지 않고도 처리량(throughput)을 높일 수 있었던 이유입니다.
8. 용량 모델(Capacity model) 및 권장 사항
용량(Capacity): 초당 약 1,300개의 쿼리(queries/second). 지속 가능한 속도를 구하기 위해 이를 평균 제출 크기로 나눕니다:
| 평균 제출 크기 | 큐잉(queueing) 발생 전 지속 가능한 초당 제출 수 |
|---|---|
| 50개 문장 | ~26 |
| ... |
지연 시간(Latency): 약 0.77 ms/문장 및 큐(queue) 대기 시간. 지속적인 부하가 한계치에 도달하기 전까지 100개 문장 문서의 경우 100 ms 미만이며, 그 이후에는 백로그(backlog)와 함께 선형적으로 증가합니다.
운영 가이드라인:
- 실제 트래픽은 거의 확실히 1,300 q/s보다 훨씬 낮으므로 현재로서는 조치가 필요하지 않습니다.
- p99가 수용 가능한 수준을 유지하는 범위 내에서 지속적인 동시성(sustained concurrency)을 유지하십시오. 55개 워커(worker) 지점은 이미 큐 영역(p99 약 1.9 s)에 진입해 있었습니다.
- 대규모 제출(large-submission)로 인한 지연 시간이 사용자 경험(UX)을 해치고 서버가 포화 상태가 아니라면, 하나의 제출을 여러 개의 작은 동시 요청으로 분할하십시오. 서버 비용은 동일하면서 실제 소요 시간(wall-clock)은 줄어듭니다.
한계치를 높이기 위한 레버(Levers) (저비용 이점인 양자화(quantization), RAM 내 HNSW(HNSW-in-RAM), 샤딩(sharding) 및 병렬화(parallelization)는 이미 모두 적용되었습니다):
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기