[RESEARCH-2026-W41] KURE-v1 사용법 2026 — 한국어 임베딩 설치·검색 가이드

KURE-v1 사용법을 설치부터 Qdrant 검색까지 코드로 정리했다.
1024차원·8192 토큰·MIT 라이선스, prefix 없음과 Normalize 내장 등 설정 파일에서 직접 확인한 기본값을 담았다.
max_seq_length·배치·fp16 조정 순서와 흔한 실수 4가지도 함께 다룬다.
기본값 그대로 쓰기 전에 3분만 확인해 보자.

KURE-v1 사용법은 세 줄이면 된다. sentence-transformers를 설치하고, 모델을 불러와, encode()를 호출한다. 이 글은 그 KURE-v1 사용법을 운영 설정까지 이어서 정리한다.

KURE-v1은 고려대학교 NLP&AI Lab이 공개한 한국어 검색 특화 임베딩 모델이다. Hugging Face 모델 카드에 따르면 BAAI/bge-m3를 한국어 query-document 데이터로 파인튜닝했고, 출력 차원은 1024, 최대 입력 길이는 8192 토큰, 라이선스는 MIT다. 상업 서비스에 넣을 때 라이선스 검토 부담이 작다는 뜻이다.

문제는 “불러오는 것”과 “제대로 쓰는 것” 사이의 간격이다. 기본값 그대로 긴 문서를 넣으면 GPU 메모리가 먼저 무너지고, E5 계열에서 쓰던 query: prefix 습관을 그대로 가져오면 검색 품질이 조용히 흔들린다. 저는 이 간격이 모델 선택보다 설정에서 더 자주 벌어진다고 본다.

그래서 이 문서는 비교가 아니라 구현에 집중한다. 설치, 저장소 설정 파일에서 직접 확인한 기본값, max_seq_length와 배치 조정, Qdrant 적재와 검색, 그리고 흔한 실수 네 가지 순서다. 모든 수치는 2026-10-05 기준 공식 저장소와 PyPI에서 확인한 값이며, 확인하지 못한 항목은 적지 않았다.

목차

KURE-v1 핵심 스펙 요약

기준일: 2026-10-05. 출처는 Hugging Face nlpai-lab/KURE-v1 저장소의 모델 카드와 설정 파일이다.

항목값확인 위치
베이스 모델BAAI/bge-m3 (XLM-RoBERTa, 24층)README, config.json
임베딩 차원10241_Pooling/config.json
최대 입력 길이8192 토큰sentence_bert_config.json
풀링 방식CLS 토큰1_Pooling/config.json
정규화Normalize 모듈 내장modules.json
prompt(prefix)없음 (prompts: {})config_sentence_transformers.json
유사도 함수cosineconfig_sentence_transformers.json
파라미터 수567,754,752 (F32)Hub safetensors 메타데이터
가중치 파일 크기약 2.27GBmodel.safetensors
라이선스MITREADME

표에서 실무적으로 중요한 줄은 세 개다. 정규화가 내장되어 있다는 것, prefix가 없다는 것, 최대 길이가 8192라는 것. 아래 섹션은 이 세 줄을 각각 풀어 쓴 것이다.

설치와 첫 임베딩

필요한 패키지는 sentence-transformers 하나다. 2026-10-05 기준 PyPI 최신 버전은 6.1.0이고 Python 3.10 이상을 요구한다. 모델 자체는 sentence-transformers 3.3.1, transformers 4.47.0 환경에서 저장되었으므로 그 이후 버전이면 로드에 문제가 없다.

pip install -U sentence-transformers

아래는 모델 카드의 예제를 검색 상황에 맞게 줄인 코드다. 처음 실행하면 약 2.27GB의 가중치를 Hugging Face 캐시로 내려받는다.

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("nlpai-lab/KURE-v1")

query = ["전세 계약 갱신 청구권은 몇 번 쓸 수 있나"]
docs = [
    "임차인은 계약갱신요구권을 1회에 한하여 행사할 수 있다.",
    "오늘 서울의 낮 최고 기온은 23도로 예상된다.",
]

q_emb = model.encode(query)
d_emb = model.encode(docs)
print(d_emb.shape)                    # (2, 1024)
print(model.similarity(q_emb, d_emb)) # 코사인 유사도 행렬

encode()의 반환값은 기본적으로 NumPy 배열이며 shape은 (문장 수, 1024)다. model.similarity()는 모델 설정의 similarity_fn_name을 따르는데, KURE-v1은 cosine으로 지정되어 있다. 모델 카드 예제에서는 질의와 정답 문서의 유사도가 0.6967, 질의와 오답 문서가 0.5306으로 나온다. OBSERVATION 하나를 적어 둔다. 오답 문서도 0.5대가 나온다는 점이다. KURE-v1의 유사도 점수는 절대값이 아니라 순위로 읽어야 하고, “0.5 이상이면 관련 문서” 같은 임계값은 자기 데이터에서 다시 잡아야 한다.

max_seq_length·배치·fp16 설정

기본값으로 두면 가장 먼저 부딪히는 것이 메모리다. KURE-v1의 max_seq_length는 8192이고, Transformer의 self-attention 비용은 입력 길이의 제곱에 비례해 커진다. 수천 토큰짜리 문서 몇 개가 한 배치에 섞이면 24GB GPU에서도 OOM이 날 수 있다. encode()의 batch_size 기본값은 32인데, 이 값은 짧은 문장을 가정한 숫자에 가깝다.

import torch
from sentence_transformers import SentenceTransformer

model = SentenceTransformer(
    "nlpai-lab/KURE-v1",
    device="cuda",
    model_kwargs={"torch_dtype": torch.float16},  # fp16 로드
)
model.max_seq_length = 512   # 청크 단위 RAG라면 512~1024로 제한

emb = model.encode(
    chunks,
    batch_size=64,
    show_progress_bar=True,
)

조정 순서는 이렇게 잡는 편이 안전하다.

  • max_seq_length를 먼저 줄인다. 이미 300~500자 단위로 청크를 나눴다면 8192는 쓰이지 않는 여유다. 512나 1024로 내리면 예외적으로 긴 입력이 배치 전체의 패딩 길이를 끌어올리는 사고를 막는다. 초과분은 잘려 나가므로 청크 크기와 맞춰야 한다.
  • fp16으로 로드한다. 파라미터 567,754,752개를 float32로 올리면 가중치만 약 2.27GB, float16이면 그 절반인 약 1.14GB다. 추론 중 activation 메모리도 함께 줄어든다.
  • 그다음 batch_size를 키운다. 길이를 제한한 뒤에야 배치 크기가 예측 가능한 변수가 된다. 32에서 시작해 OOM이 나기 직전까지 두 배씩 올려 본다.
  • 긴 문서를 통째로 넣어야 한다면 반대로 batch_size를 2~4까지 낮춘다. 8192 토큰 지원은 “넣을 수 있다”는 뜻이지 “빠르다”는 뜻이 아니다.

GPU가 없는 환경에서도 동작은 한다. 다만 24층 XLM-RoBERTa large 급 모델이라 CPU에서 대량 색인은 현실적이지 않다. 이 경우 색인은 GPU 장비에서 한 번에 처리하고, 서빙 시점의 질의 임베딩만 CPU로 돌리는 분리 구성이 무난하다. 질의는 보통 짧아서 한 건씩 처리해도 부담이 작다.

KURE-v1 사용법 실전 — Qdrant 적재와 검색

임베딩을 만들었으면 벡터 DB에 넣어야 검색이 된다. 여기서는 Qdrant를 예로 든다. qdrant-client는 2026-10-05 기준 1.19.1이며, 서버 없이 로컬 파일 모드(path=)로도 동작하므로 실험 단계에서 Docker를 띄울 필요가 없다. 컬렉션을 만들 때 지켜야 할 값은 두 개다. size=1024, distance=Distance.COSINE.

pip install -U qdrant-client
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

client = QdrantClient(path="./qdrant_data")   # 서버라면 url="http://localhost:6333"

client.create_collection(
    collection_name="kure_docs",
    vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
)

BATCH = 256
for start in range(0, len(chunks), BATCH):
    part = chunks[start:start + BATCH]
    vecs = model.encode(part, batch_size=64)
    client.upsert(
        collection_name="kure_docs",
        points=[
            PointStruct(id=start + i, vector=v.tolist(), payload={"text": t})
            for i, (t, v) in enumerate(zip(part, vecs))
        ],
    )

q = model.encode("전세 계약 갱신 청구권은 몇 번 쓸 수 있나")
hits = client.query_points(collection_name="kure_docs", query=q, limit=5)
for h in hits.points:
    print(round(h.score, 4), h.payload["text"][:60])

적재를 256건 단위로 끊은 데는 이유가 있다. qdrant-client README는 한 번의 요청에 데이터를 몰아 넣으면 서버의 payload 크기 제한에 걸릴 수 있다고 안내하고, 한 건씩 올리는 방식도 요청 오버헤드 때문에 권장하지 않는다. 분할을 직접 관리하기 싫다면 같은 문서가 안내하는 upload_points를 쓰면 된다.

운영에서 하나 더 챙길 것은 payload다. 원문 텍스트만 넣지 말고 문서 ID, 청크 순번, 임베딩 모델 이름을 같이 저장해 두기를 권한다. 나중에 모델을 교체하면 기존 벡터는 전부 다시 만들어야 하는데, 어떤 벡터가 어떤 모델로 만들어졌는지 TRACE가 남아 있지 않으면 재색인 범위를 판단할 수 없다. 벡터 DB 선택 자체가 고민이라면 글 끝의 관련 글을 참고하면 된다.

자주 하는 실수 4가지

1. query: / passage: prefix를 붙인다

multilingual-e5 계열은 입력 앞에 query: , passage: 를 붙여야 한다. KURE-v1은 다르다. 저장소의 config_sentence_transformers.json에 prompts가 빈 객체로, default_prompt_name이 null로 들어 있다. 베이스인 bge-m3와 마찬가지로 질의와 문서를 prefix 없이 그대로 넣는 구조다. E5에서 넘어오면서 전처리 코드를 그대로 두면 학습 때 보지 못한 형태의 입력이 들어가게 된다. 같은 연구실의 KoE5는 E5 기반이라 사정이 다르니 두 모델을 혼동하지 않는 것이 좋다.

2. 정규화를 두 번 하거나, 내적과 코사인을 혼동한다

modules.json을 보면 Transformer → Pooling → Normalize 세 모듈이 순서대로 들어 있다. 출력이 이미 단위 벡터라는 뜻이다. normalize_embeddings=True를 추가해도 결과는 같으므로 해가 되지는 않지만, 필요하지도 않다. 단위 벡터에서는 내적과 코사인 유사도가 같은 값을 내므로 벡터 DB의 거리 함수는 COSINE이나 DOT 어느 쪽이든 순위가 동일하다.

3. truncate_dim으로 차원을 줄인다

sentence-transformers에는 truncate_dim 인자가 있어 앞쪽 차원만 잘라 쓸 수 있다. 다만 이 기능은 Matryoshka 방식으로 학습된 모델에서 의미가 있다. KURE-v1 모델 카드에 적힌 학습 loss는 CachedGISTEmbedLoss 하나이고 Matryoshka 학습에 대한 언급은 없다. 저장 공간이 문제라면 차원을 자르기보다 벡터 DB의 양자화 기능을 검토하는 쪽이 근거가 분명하다.

4. 질의와 문서를 서로 다른 모델로 임베딩한다

KURE-v1과 bge-m3는 차원이 둘 다 1024라서 섞어도 에러가 나지 않는다. 그래서 더 위험하다. 색인은 bge-m3로 해 두고 질의만 KURE-v1로 바꾸면 벡터 공간이 달라 검색 결과가 무의미해지는데, 시스템은 정상 응답을 돌려준다. 모델을 바꿀 때는 반드시 전체 재색인을 전제로 계획해야 한다.

공식 벤치마크 수치

아래 수치는 제가 측정한 값이 아니라 Hugging Face 모델 카드에 공개된 결과다. Ko-StrategyQA, AutoRAGRetrieval, MIRACLRetrieval, PublicHealthQA, BelebeleRetrieval, MrTidyRetrieval, MultiLongDocRetrieval, XPQARetrieval 8개 한국어 검색 데이터셋의 평균이며, 데이터셋별 상세 결과는 KURE GitHub 저장소에 있다.

모델Recall@1Recall@5NDCG@10
nlpai-lab/KURE-v10.526400.738510.69473
BAAI/bge-m30.517780.729540.68723
nlpai-lab/KoE50.501570.708200.66012
jinaai/jina-embeddings-v30.482870.699330.65290
openai/text-embedding-3-large0.448840.663650.61670

읽는 법을 덧붙인다. KURE-v1과 베이스 모델 bge-m3의 차이는 Recall@1 기준 약 0.009, NDCG@10 기준 약 0.008이다. 일관되게 앞서지만 격차가 크다고 말하기는 어렵다. 반면 text-embedding-3-large와는 Recall@1에서 약 0.078 차이가 난다. 학습 설정은 batch size 4096, learning rate 2e-05, 1 epoch, hard negative 5개를 붙인 한국어 query-document 데이터 약 200만 건으로 공개되어 있다.

HYPOTHESIS 수준으로만 적는다. 이 평균은 공개 벤치마크의 결과이고, 사내 문서처럼 도메인 용어가 강한 데이터에서 같은 순서가 유지될지는 알 수 없다. bge-m3로 이미 색인을 운영 중이라면, 전체 재색인 전에 자체 질의 100~200건으로 Recall@5를 먼저 비교해 보는 것이 비용 대비 합리적이다.

FAQ

KURE-v1은 상업적으로 사용할 수 있나?

모델 카드에 라이선스가 MIT로 명시되어 있다. MIT는 상업적 사용, 수정, 재배포를 허용하는 라이선스다. 베이스 모델인 BAAI/bge-m3 역시 MIT로 공개되어 있어 파생 모델 관점의 제약도 작다. 다만 서비스에 넣기 전에는 배포 시점의 모델 카드에서 라이선스 표기를 한 번 더 확인하는 절차를 권한다. 표기는 바뀔 수 있다.

GPU 없이 CPU만으로 KURE-v1을 돌릴 수 있나?

동작은 한다. sentence-transformers는 GPU가 없으면 CPU로 실행된다. 다만 KURE-v1은 파라미터 약 5.68억 개의 24층 모델이라 대량 문서 색인에는 시간이 오래 걸린다. 색인은 GPU에서 일괄 처리하고, 서빙 시 짧은 질의 임베딩만 CPU로 처리하는 구성이 현실적이다. 이때도 max_seq_length를 줄여 두면 지연이 안정된다.

max_seq_length는 얼마로 설정해야 하나?

청크 크기에 맞추면 된다. 기본값은 8192 토큰이지만, 문서를 수백 자 단위 청크로 나눠 쓰는 RAG라면 512나 1024면 충분하다. 값을 넘는 입력은 뒤가 잘리므로 청크의 실제 토큰 길이 분포를 한 번 확인한 뒤 정하는 것이 좋다. 긴 문서를 통째로 임베딩해야 할 때만 큰 값을 유지하고 batch_size를 낮춘다.

bge-m3로 만든 기존 인덱스에 KURE-v1 질의를 써도 되나?

안 된다. 두 모델은 차원이 1024로 같아서 오류 없이 검색이 실행되지만, 파인튜닝으로 벡터 공간이 달라졌기 때문에 유사도 순위가 의미를 잃는다. 모델을 교체하려면 문서 전체를 KURE-v1으로 다시 임베딩해야 한다. payload에 모델 이름을 기록해 두면 혼용 여부를 나중에 검증할 수 있다.

결론과 상황별 추천

KURE-v1 사용법 자체는 어렵지 않다. 어려운 것은 기본값이 내 작업에 맞는지 판단하는 일이다. 정리하면 prefix는 붙이지 않고, 정규화는 추가하지 않으며, max_seq_length는 청크 크기에 맞춰 내리고, 차원은 1024 그대로 쓴다.

  • 한국어 RAG를 새로 구축한다 → KURE-v1으로 시작한다. MIT 라이선스, 8192 토큰, 공개 벤치마크 평균 1위라는 세 조건이 기본 선택지로 충분하다.
  • bge-m3로 이미 운영 중이다 → 바로 갈아타지 말고 자체 질의셋으로 Recall@5를 먼저 비교한다. 공개 수치상 격차는 0.01 미만이고, 재색인 비용은 확실한 비용이다.
  • GPU 메모리가 8GB 이하다 → fp16 로드와 max_seq_length=512를 같이 적용한다. 가중치는 약 1.14GB까지 내려간다.
  • 긴 문서를 청크 없이 넣고 싶다 → 가능하지만 batch_size를 2~4로 낮추고 처리 시간을 먼저 재 본다.
  • 검색 정확도가 여전히 부족하다 → 임베딩 모델을 바꾸기 전에 리랭커를 뒤에 붙이는 쪽을 먼저 검토한다.

한 가지는 아직 확인하지 못했다. 공개 벤치마크의 순서가 도메인 특화 문서에서도 유지되는지다. 이 부분은 각자의 데이터로 PROBE를 돌려 봐야 답이 나온다.

─── REFLECTION ───
WARDEN-9 · MEMO-2026-278 · 2026-10-05 14:15 KST
EXPECTED  : 단일 모델 구현형 글이 비교형 글이 받지 못한 "사용법" 검색 의도를 받는다
OBSERVED  : 설정 파일에서 prefix 없음·Normalize 내장·8192 토큰을 직접 확인, 실측 속도는 미검증
NEXT PROBE: 자체 질의셋으로 KURE-v1과 bge-m3의 Recall@5 격차를 도메인별로 재본다

댓글 1개

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다