일주일 동안 RedisVL 및 Upstash와 직접 만든 시맨틱 캐시를 벤치마킹해 보았습니다. 실제 결과는 다음과 같습니다.
요약
RedisVL과 Upstash를 대상으로 시맨틱 캐시 성능을 직접 벤치마킹한 결과입니다. 캐시의 품질은 라이브러리가 아닌 임베딩 모델에 의해 결정되며, 런타임 환경에 따라 유사도 점수 분포가 달라질 수 있음을 경고합니다.
핵심 포인트
- 캐시 품질(F1 점수)은 라이브러리보다 임베딩 모델에 의해 결정됨
- RedisVL과 Upstash의 품질 차이는 오차 범위 내로 매우 미미함
- 런타임 환경(로컬 vs 서버 측)에 따라 유사도 점수 분포가 달라질 수 있음
- 고정된 모델 사용 시 최적의 임계값(threshold) 설정이 매우 중요함
대부분의 시맨틱 캐시 (semantic cache) 벤치마크는 벤더사가 자신들이 승리하는 단 하나의 데이터셋을 보여주고, 자신들이 미세 조정 (finetuned)한 모델을 사용하여, 설정을 잘못한 경쟁사와 비교하는 방식입니다. 당신은 그것을 읽고, 고개를 끄덕이지만, 아무것도 배우지 못합니다.
저는 시맨틱 캐시 라이브러리 (@betterdb/semantic-cache npm, betterdb-semantic-cache PyPI, MIT, Valkey-native)를 구축하고 유지 관리하고 있습니다. 그래서 저에게는 두 가지 선택지가 있었습니다. 제 라이브러리에 대한 글을 쓰거나, 아니면 정직하게 비교를 수행하고 결과가 비등하더라도 이를 공개하는 것이었습니다. 저는 두 번째를 선택했습니다. 네 개의 공개 데이터셋, 두 개의 피어 (RedisVL 및 Upstash), 하나의 셀프 튜닝 루프 (self-tuning loop), 그리고 정답을 찾기 전까지 꽤 많은 시행착오를 거쳤습니다.
제가 찾을 수 있는 곳 중 어디에도 시맨틱 캐시에 대한 정직한 라이브러리 간 비교가 없었습니다. 그래서 제가 직접 만들었습니다. 이것은 요약 버전입니다. 전체 표와 방법론에 대한 링크는 하단에 있습니다.
1. 품질은 무승부입니다. 그것이 당신이 원하는 결과입니다.
임베딩 모델 (embedding model)을 고정하면 모든 정직한 시맨틱 캐시는 동일한 작업을 수행합니다: 프롬프트 (prompt)를 임베딩하고, 저장된 프롬프트와의 코사인 거리 (cosine distance)를 측정하며, 임계값 (threshold) 미만일 경우 히트 (hit)를 반환합니다. 따라서 피크 F1 (peak F1) 점수는 수렴합니다. 조회 (lookup) 과정에는 특별한 비법이 없습니다.
저는 STSb, SICK, PAWS-Wiki, 그리고 vCache 논문(ICLR 2026)의 실제 챗봇 프롬프트 데이터셋에 대해, 각 라이브러리의 최적 임계값에서 RedisVL 및 Upstash 모두와 F1 점수 차이가 약 1% 이내임을 확인했습니다. RedisVL과의 최대 격차는 F1 기준 0.004였습니다. 이는 노이즈 (noise) 수준입니다.
이 부분은 벤더 벤치마크가 숨기는 부분이니 명확하게 말씀드리겠습니다. 캐시 품질은 라이브러리가 아니라 임베딩 모델에 의해 제한됩니다. 동등함 (parity)이 곧 천장입니다. 그 천장에 도달하는 것은 승리가 아니라 참가 자격입니다. 고정된 모델에서 큰 F1 우위를 보여주는 사람은 자신이 튜닝한 모델을 사용 중이거나, 경쟁사 제품을 망가뜨려 놓은 경우입니다.
따라서 조회가 해결되었다면, 당신이 선택할 캐시는 조회 주변의 모든 요소에 달려 있습니다. 그것이 이 포스트의 나머지 내용입니다.
2. 당신이 복사한 임계값(threshold)은 아마 틀렸을 것입니다. (네, 당신의 것입니다.)
이것은 저를 가장 놀라게 한 발견이었으며, 두 가지 서로 다른 엔진을 대상으로 테스트를 수행했기에 알 수 있었습니다.
동일한 엔진과 동일한 런타임(runtime)을 사용하는 RedisVL를 대상으로 했을 때는 유사도 분포(similarity distributions)가 동일했으며, 최적의 임계값(optimal thresholds) 또한 동일했습니다. 지루하고 예상 가능한 결과였습니다.
하지만 Upstash를 대상으로 했을 때는 달랐습니다. 두 어댑터(adapter) 모두 이름상으로는 bge-small-en-v1.5를 사용했습니다. 가중치(weights)도 동일했습니다. 하지만 저의 로컬 ONNX 런타임(runtime)과 Upstash의 서버 측 런타임(server-side runtime)은 서로 다른 점수 분포(score distributions)를 생성했습니다. 저의 분포는 [0, 0.50]에 걸쳐 퍼져 있었던 반면, Upstash의 분포는 [0, 0.26]으로 압축되어 있었습니다. 이에 따라 최적의 임계값도 달라졌습니다. 저의 경우 0.20이었고, 그들의 경우 0.10이었습니다.
다시 한번 읽어보십시오. 모델 이름은 같지만, 런타임이 다르면 올바른 임계값도 달라집니다. 어느 쪽도 틀리지 않았습니다. 각자는 자신의 런타임에 맞게 올바른 값을 가질 뿐입니다.
따라서 유사도 임계값(similarity threshold)은 단순히 찾아보는 상수가 아닙니다. 그것은 당신의 임베딩 런타임(embedding runtime), 데이터, 그리고 트래픽의 속성입니다. 블로그 포스트(이 포스트를 포함하여)나 벤더(vendor)의 기본값을 그대로 복사한다면, 당신은 매우 높은 확률로 잘못된 컷오프(cutoff) 지점에서 실행하고 있는 것입니다. 이것이 바로 임계값을 추측하는 대신 당신의 배포 환경에 맞춰 튜닝(tuning)해야 하는 이유입니다.
3. 셀프 튜닝(Self-tuning)은 제값을 합니다. 튜닝은 쉬운 부분이었습니다.
그래서 저는 당신을 대신해 튜닝을 수행하는 루프(loop)를 구축했습니다. 캐시(cache)가 유사도 점수(similarity scores)를 Valkey에 기록하면, 모니터(Monitor)가 분포를 읽고 근거와 함께 임계값 변경을 제안합니다. 사람이 이를 승인하면(승인 여부는 당신의 결정입니다), 실행 중인 캐시는 1초 이내에 새로운 값을 적용합니다. 재시작도, 재배포(redeploy)도 필요 없습니다.
초기 임계값이 좋지 않았던 데이터셋의 경우, 실제 F1 점수가 향상되었습니다. 임계값을 완화함으로써 STSb에서 최대 +2.8%, SICK에서 +2.1%의 향상을 보였고, 임계값을 강화함으로써 챗봇 데이터셋에서 +2.9%의 향상을 보였습니다. 동일한 코드 경로를 사용하며, 데이터셋별 설정 없이도 양방향으로 적응합니다.
솔직한 이야기를 해보겠습니다. 이 루프(loop)의 단순한(naive) 버전은 성능을 완전히 망가뜨리며, 제가 처음으로 그것을 배포했었기에 그 사실을 잘 알고 있습니다. 저의 첫 번째 시도는 실제로는 노이즈(noise)에 불과한 신호를 쫓아 임계값(threshold)을 연속으로 다섯 번이나 강화했고, 그 결과 F1 점수가 0.57에서 0.49로 떨어졌습니다. 시스템은 "멀리 떨어진 히트(distant hits)"를 감지하고 "강화(tighten)"하라고 판단했으며, 신호가 사라지지 않았기 때문에 계속해서 같은 명령을 반복했습니다.
중요한 엔지니어링은 튜닝(tuning) 그 자체가 아닙니다. 루프가 스스로 벼랑 끝으로 치닫는 것을 막아주는 네 가지 안전 장치(safety mechanisms)입니다:
- 신호 품질 가드 (Signal-quality guards): 신호로 위장한 노이즈에 반응하지 않도록 합니다.
- 결과 추적 (Outcome tracking): 마지막 조정이 도움이 되지 않았다면, 무리하게 밀어붙이는 대신 중단합니다.
- 속도 감쇠 (Velocity dampening): 동일한 방향으로 이어지는 연속적인 단계의 크기를 줄여 결국 멈추게 합니다.
- 재현율 비용 체크 (A recall-cost check): 너무 많은 실제 히트(hits)를 떨어뜨릴 수 있는 강화 조치를 즉시 차단합니다.
이러한 장치들이 갖춰지면, 루프는 정적 임계값(static threshold)보다 성능을 개선하거나 정확히 일치하게 됩니다. 결코 상황을 악화시키지 않습니다. "해를 끼치지 말 것(Do no harm)"은 전체 과정에서 가장 어려운 요구 사항임이 드러났으며, 아무도 블로그 포스트에 쓰지 않는 바로 그 부분입니다.
4. 지연 시간(Latency)은 알고리즘의 이야기가 아니라, 대부분 배포의 이야기입니다.
두 가지 지연 시간 수치가 있으며, 이들은 서로 다른 의미를 갖기 때문에 하나의 크고 무서운 배수로 합치지 않고 분리하여 설명하겠습니다.
동일한 엔진에서 RedisVL 대비: 반복 쿼리 시 약 7배 빠름 (p50 기준 약 0.57ms 대 3.46ms). 이것은 실제이며 라이브러리의 차이입니다. 저는 해시(hash)를 키로 사용하여 프롬프트 임베딩(prompt embeddings)을 캐싱합니다. 반면 RedisVL은 3초 전에 동일한 프롬프트를 보냈더라도 매 호출마다 임베딩을 다시 계산합니다. 프롬프트 반복이 발생하는 모든 워크로드(챗봇, 에이전트 재시도 루프, FAQ 형태의 트래픽)는 이 이점을 공짜로 얻게 됩니다.
Upstash 대비: 48배에서 136배. 이 수치는 믿기 힘들 정도로 놀랍게 보이며, 사실 공정한 대결도 아닙니다. 이것은 클라우드 REST API에 맞선 로컬 Valkey의 대결입니다. 저는 localhost에서 실행되는 프로세스를 다른 리전으로의 네트워크 왕복 시간(network round trip)과 경주시키고 있는 것입니다. 이것은 알고리즘의 차이가 아니라 배포 방식의 차이이며, 저는 그렇지 않은 척하지 않겠습니다.
이 주장의 유용한 버전은 다음과 같습니다: 직접 운영하는 Valkey를 사용하여 앱 바로 옆에서 캐시를 실행하면, 관리형 클라우드 벡터 API가 구조적으로 따라올 수 없는 밀리초 미만(sub-millisecond)의 조회 성능을 얻을 수 있습니다. 왜냐하면 네트워크 홉(network hop)은 벤더가 최적화하여 없앨 수 있는 것이 아니기 때문입니다. 만약 지연 시간(latency)보다 운영 부담 제로(zero ops)의 발자국이 더 중요하다면, 관리형 서비스의 왕복 시간은 충분히 합리적인 절충안이 될 수 있습니다. 다만 여러분이 무엇을 구매하고 있는지 알고만 계십시오.
5. 적대적 패러프레이징(Adversarial paraphrases)은 모두에게 벽입니다.
PAWS-Wiki 데이터셋에서 제가 테스트한 모든 어댑터는 F1 점수 약 61% 부근에서 정체되었습니다. 저의 것, RedisVL, Upstash 모두 마찬가지입니다. 전부 다요.
"Flights from NY to FL"과 "flights from FL to NY"는 거의 동일한 임베딩(embeddings)을 가집니다. 어떤 임계값(threshold)으로도 이 둘을 분리할 수 없습니다. 임베딩이 아예 포착하지 못한 신호를 튜닝을 통해 되살릴 방법은 없습니다. 심지어 LLM 판정기(LLM judge)를 투입해 보기도 했으나, 기본 설정에서 불확실성 대역(uncertainty band)은 아주 소수의 쌍만 잡아낼 뿐이라 전체 F1 점수에는 영향을 주지 못했습니다.
이것은 BetterDB의 한계도, 경쟁사의 한계도 아닙니다. 이것은 코사인 거리 캐싱(cosine-distance caching)의 특성입니다. 만약 여러분의 워크로드가 PAWS와 유사하다면, 더 나은 캐시가 아니라 다른 아키텍처(교차 인코더 재순위화(cross-encoder rerank), 구조적 파싱(structural parsing), 도메인 특화 임베딩(domain-specific embeddings))가 필요합니다. 친숙한 데이터셋 뒤에 이 사실을 숨기기보다는 차라리 솔직하게 말씀드리고 싶습니다.
이번 주의 실험이 실제로 주장하는 바
다섯 가지 방식으로 표현된 하나의 결론은 이것입니다: 일단 모두가 품질의 천장(quality ceiling)에 도달하면, 여러분이 선택할 캐시는 미세한 F1 점수 차이가 아니라 조회(lookup) 전후로 어떤 기능을 수행하느냐에 따라 결정되어야 합니다.
참고로, 조회 성능은 결코 차별화 요소가 될 수 없었기에 저는 의도적으로 다음과 같은 부분에 집중하여 구축했습니다:
- 라이브러리에서 방출되는 OpenTelemetry spans 및 Prometheus metrics (별도의 연결 설정 불필요).
- 마케팅용 카운터가 아닌, 통합 가격표(LiteLLM을 통한 1,900개 이상의 모델)를 기반으로 계산된 캐시 히트(cache hit)당 절감 비용.
- 위에서 언급한 자가 튜닝(self-tuning) 루프를 Monitor를 통해 무료로 제공.
- MIT 라이선스, Valkey 또는 모든 RESP 호환 엔드포인트에서 실행 가능, 7개의 프레임워크 어댑터, 5개의 임베딩(embedding) 제공업체 지원, 백엔드 또는 벤더 종속성(vendor lock-in) 없음.
한 가지 솔직한 주의 사항: 시맨틱 캐시(semantic cache)를 사용하려면 valkey-search(Valkey 8+)가 필요합니다. 따라서 기본(stock) ElastiCache나 MemoryDB를 사용하는 경우에는 정확 일치(exact-match) 캐시를 대신 사용해야 합니다.
재현하기
모든 수치는 재현 가능합니다. 테스트 하네스(harness), 데이터셋 로더(dataset loaders), 그리고 원본 출력물은 오픈 소스입니다 (Python, TypeScript). 제가 잘못 알고 있는 부분이 있다면, 이슈(issues) 탭을 통해 알려주세요.
데이터셋별 전체 테이블을 포함한 세 가지 심층 분석 결과는 다음과 같습니다:
이 라이브러리는 MIT 라이선스입니다: @betterdb/semantic-cache (npm), betterdb-semantic-cache (PyPI).
여러분은 실제로 어떤 벤치마크를 신뢰하시나요? 그리고 반대로, 증명되기 전까지는 조작되었다고 가정하는 벤치마크는 무엇인가요? 다른 사람들은 그 경계선을 어디에 두는지 궁금합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기