
「연차는 다음 해로 며칠까지 넘길 수 있나요?」를 사내 규정 문서에서 임베딩으로만 찾으면 연차 신청 절차, 휴가 종류 안내, 작년 공지가 위에 섭니다. 셋 다 연차를 다루니 질문 벡터와 가깝습니다. 이월 한도가 적힌 문단은 7위쯤에 있을 수 있습니다. 모델에게는 상위 몇 건만 실리므로, 정답 문단이 그 밖에 있으면 모델은 작년 공지의 숫자로 답합니다.
OwlAI의 문서 검색은 순서를 두 단계로 정합니다. 첫 단계는 의미 검색과 키워드 검색으로 후보를 넓게 모읍니다. 둘째 단계에서 리랭커가 질문과 후보를 함께 읽고 다시 정렬합니다. 리랭커가 늦거나 실패하면 곧바로 첫 단계의 순서를 냅니다. 이 글은 리랭커가 왜 순서를 고칠 수 있는지, 어디에 두고 언제 포기하는지, 모델을 무엇으로 고르는지를 다룹니다.
임베딩 거리는 주제가 비슷한지를 잰다
임베딩 검색은 질문과 청크를 따로 벡터로 만듭니다(바이 인코더, bi-encoder). 청크 벡터는 문서를 올릴 때 미리 만들어 두므로, 청크를 벡터 하나로 줄이는 시점에는 어떤 질문이 올지 모릅니다. 「연차」라는 주제는 벡터에 남지만, 「며칠까지 넘길 수 있나」에 답하는 문장이 그 안에 있는지는 흐려집니다. 그래서 거리는 같은 주제의 청크끼리 촘촘하게 모이고, 그 안의 순서는 질문에 답하는지와 상관없이 정해집니다.
흔한 대응은 둘입니다. 하나는 더 좋은 임베딩 모델로 바꾸는 것입니다. 실제로 효과가 있습니다. 리랭커 없이 임베딩 모델만 바꿨을 때 상위 5건의 품질(nDCG@5)이 0.420에서 0.500으로 올랐습니다(측정 조건은 아래 「실측」 절). 하지만 언어마다 최적 모델이 다르고, 바꿔도 같은 주제의 청크가 촘촘히 모이는 성질은 그대로입니다. 다른 하나는 모델에게 더 많이 싣는 것입니다. 상위 20건을 실으면 정답이 들어올 가능성은 커지지만, 늘린 만큼 컨텍스트를 쓰고 같은 주제의 다른 문단도 함께 실려 모델이 그중 하나를 근거로 고를 수 있습니다.
그래서 첫 단계에는 정답을 후보 안에 넣는 일만 맡기고, 순서는 다른 모델이 정합니다.
첫 단계는 후보를 넓게 모은다
의미 검색은 pgvector에 저장한 청크 벡터와 질문 벡터의 거리로 50건을, 키워드 검색은 PostgreSQL 전문 검색으로 50건을 뽑습니다. 두 목록은 RRF(상호 순위 융합)로 합칩니다. 두 점수는 척도가 달라 더할 수 없지만 순위는 더할 수 있습니다(「테이블을 잘못 고른 SQL도 실행된다, 검색이 후보부터 좁힌다」). 키워드 검색이 한국어 질문을 어떻게 질의로 바꾸는지는 「조사 붙은 검색어는 전문 검색에 안 걸린다, 낱말을 OR로 잇는다」에서 다룹니다.
합친 목록은 모델이 도구 호출에서 요청한 개수(기본 8건, 최대 30건)로 자릅니다. 리랭커가 켜져 있으면 그 개수와 리랭커 후보 수(topKInput) 가운데 큰 쪽까지 남깁니다. 이 목록의 목표는 순서가 아니라 포함입니다. 이월 한도 문단이 7위로 들어와 있으면 첫 단계는 할 일을 다 한 것입니다.
리랭커는 질문과 청크를 한 입력으로 읽는다
리랭커는 질문과 청크를 한 입력으로 함께 읽고, 이 청크가 이 질문에 답하는지 점수를 냅니다(크로스 인코더, cross-encoder). 질문의 「며칠까지」와 청크의 「최대 10일까지 이월」이 같은 입력 안에서 서로를 보므로, 주제만 같은 청크와 답이 든 청크를 가를 수 있습니다. 대신 청크 벡터처럼 미리 계산해 둘 수 없고 후보마다 모델을 돌려야 해서, 좁힌 후보에만 씁니다.
설정 파일의 reranker 블록이 이 단계를 정합니다.
| 설정 | 설정 예시의 값 | 하는 일 |
|---|---|---|
topKInput |
20 | 첫 단계 목록의 앞쪽 몇 건까지 리랭커에 보낼지. 요청 개수가 더 크면 요청 개수 |
timeoutMs |
자체 호스팅 3000, 관리형 5000 | 이 시간이 지나면 폴백한다 |
리랭커 자리에는 Cohere 형식의 /rerank 엔드포인트를 여는 서버라면 무엇이든 붙일 수 있습니다. 구축형은 vLLM으로 띄운 자체 호스팅 모델을, GPU가 없는 SaaS 형상은 관리형 Cohere를 씁니다. reranker 블록을 빼면 첫 단계의 순서로 동작합니다. 돌려주는 건수는 리랭커가 켜져 있든 꺼져 있든 모델이 요청한 개수입니다.
리랭커는 권한을 거른 뒤의 후보만 읽습니다. 검색과 리랭크는 사용자가 볼 수 있는 문서 모음 안에서만 돌고, 돌아온 행도 모델에 넘기기 전에 문서 모음 권한으로 한 번 더 거릅니다. 전부 리랭크한 뒤 권한으로 거르면 허용된 결과가 요청한 개수보다 적어질 수 있기 때문입니다. 권한이 없는 조직 문서 모음은 리랭크 없이 따로 찾아 문서 이름과 문서 모음 이름만 알리고, 내용은 싣지 않습니다.
리랭커가 켜져 있으면 임베딩 차이가 상위 5건에서 거의 사라진다
실측은 앱 전체를 격리 환경에 띄우고 제품의 검색 경로 그대로 했습니다(2026년 9월). 실운영 환경의 문서 청크 8,073개를 복제하고, 사용자가 실제로 던진 질의 150건을 썼습니다. 여러 구성의 상위 결과를 모아 어느 구성 것인지 가린 채 0·1·2 세 단계로 판정했고, 유의성은 질의마다 짝지은 부트스트랩으로 봤습니다. 벤치마크용 코드를 따로 쓰지 않은 이유는 키워드 검색, RRF 가중치, 권한 필터, 리랭커 입력 형식 가운데 하나만 달라져도 측정이 제품과 어긋나기 때문입니다.
임베딩 모델 두 개(자체 호스팅 Qwen3-Embedding-8B와 클라우드 관리형 모델 하나)를 리랭커를 켜고 끈 상태로 비교한 nDCG@5는 다음과 같습니다. 리랭커는 Qwen3-Reranker-0.6B, 후보는 20건입니다.
| 임베딩 A | 임베딩 B | 차이 | |
|---|---|---|---|
| 리랭커 끔 | 0.500 | 0.420 | 0.081, p=0.0055 |
| 리랭커 켬 | 0.567 | 0.544 | 0.023, p=0.39 |
임베딩 품질의 차이는 실제로 있지만, 리랭커를 켜면 통계적으로 구분되지 않을 만큼 줄어듭니다. 리랭커가 후보 20건 안에서 정답을 앞으로 되찾아 오기 때문입니다. 첫 단계에 포함만 맡기고 순서를 리랭커에 맡긴 근거가 이것입니다.
리랭커 모델은 언어와 입력 형식을 맞춰야 값을 한다
리랭커라고 다 순서를 고치지는 않습니다. 2026년 8월에 대화 메모리 청크와 모델이 만든 질의 200건으로 잰 MRR@5에서, 범용 다국어 리랭커 Qwen3-Reranker-4B는 한국어 질의에서 리랭킹 없음과 구분되지 않았습니다(0.56 대 0.52, p=0.13). 한국어로 추가 학습한 리랭커는 0.70이었고, 영어 질의에서는 두 모델의 순서가 뒤집혔습니다. 9월 평가에서도 관리형 리랭커 가운데 한국어 문서 질의에서 리랭킹 없음과 구분되지 않는 모델이 있었습니다. 두 언어에서 모두 최적인 모델은 없으므로 리랭커와 임베딩 모델은 설치 설정으로 둡니다. 자체 호스팅 기본값인 Qwen3-Reranker-0.6B는 8월 평가에서 4B와 품질이 통계적으로 같았고, 한국어 요청 지연은 2,605ms에서 447ms로 줄었습니다. 9월 평가에서는 0.6B가 리랭킹 없음을 0.57 대 0.50으로 이겼습니다. 8월의 4B 결과와 결론이 다른 것은 평가 세트가 달라서입니다.
입력 형식도 맞아야 합니다. Qwen3-Reranker는 yes/no를 답하는 언어 모델이고, 점수는 마지막 토큰에서 yes와 no의 확률 비율입니다. 이 비율은 모델이 학습 때 본 채팅 템플릿으로 감싸야 의미가 있는데, vLLM의 /v1/rerank(0.26.0에서 확인)는 이 템플릿을 씌우지 않습니다. 감싸지 않으면 점수가 0.65~0.99에 뭉칩니다. 「JSON을 저장하는 데이터베이스 기능은?」이라는 질문에 「프랑스의 수도는 파리다」가 0.95를 받아 정답 문장(0.82)보다 높았고, 템플릿을 씌우자 0.000003과 0.89로 갈렸습니다. 위 평가 세트에서는 nDCG@5가 0.34에서 0.57로 올랐습니다. 반대로 bge 계열 리랭커는 이 템플릿을 씌우면 순서가 뒤집혔습니다. 그래서 검색 클라이언트는 모델 이름이 Qwen3-Reranker 계열일 때만 보내기 전에 템플릿을 씌우고, 어느 형식으로 보냈는지 로그에 남깁니다.
리랭커가 늦거나 실패하면 RRF 순서를 그대로 낸다
리랭커가 빠져도 결과는 틀리지 않습니다. 정렬 품질만 첫 단계 수준으로 내려갑니다. 그래서 timeoutMs는 리랭커가 일을 마칠 때까지 주는 시간이 아니라, 기다리다 폴백하는 시점입니다. 관리형 리랭커를 국내에서 부르면 지연 중앙값은 1초 안팎이었지만, 드물게 17~19초가 걸리거나 30초 넘게 응답이 없는 호출이 있었습니다. 그런 호출을 기다리면 검색 전체가 멈추므로, 관리형 설정 예시는 중앙값의 약 5배인 5초에서 끊고 이미 계산해 둔 RRF 순서를 냅니다.
벤더가 멈춰 있으면 검색마다 5초를 버리게 됩니다. 이를 막으려고 리랭크 호출은 서킷 브레이커 뒤에 둡니다. 한 프로세스에서 연달아 세 번 실패하면 브레이커가 열려 요청을 보내지 않고 바로 폴백하고, 30초 뒤 시험 호출 하나로 회복을 확인합니다. 타임아웃, 전송 오류, 5xx와 함께 401·429 같은 4xx도 실패로 셉니다. 키가 틀렸거나 한도에 걸렸으면 다시 보내도 같은 결과이기 때문입니다. 다른 외부 호출의 브레이커는 조금 더 기다려서라도 성공하는 편이 나은 경로라 더 늦게 열리고, 리랭크는 빠져도 결과가 나오는 단계라 일찍 포기합니다.
검색 결과에는 리랭크를 탔는지와 폴백했다면 그 이유가 함께 실리고, 모델에게도 폴백 경고가 갑니다. 관리형 리랭커는 호출마다 과금되므로 성공한 리랭크 호출은 사용량 원장에 턴 사용자 명의로 기록됩니다. 브레이커가 열려 보내지 않은 호출은 청구되지 않으니 기록하지 않습니다.
설계에서 고른 것
리랭커는 순서만 정하고, 몇 건을 실을지는 모델이 정합니다. 반환 건수는 리랭커가 켜져 있든 꺼져 있든 모델이 검색 도구를 부를 때 요청한 개수입니다. 모델이 넓게 보려고 20건을 달라고 하면 20건이 갑니다. 리랭커가 읽는 후보는 요청 개수와 topKInput 가운데 큰 쪽이라, 기본 요청 8건이면 리랭커는 20건을 읽고 그중 8건을 돌려줍니다. 요청보다 넓게 읽어야 RRF 순서로 9~20위에 있던 정답을 앞으로 끌어올릴 수 있습니다. 대가는 리랭커가 한 번에 읽는 청크가 늘어난 만큼 지연이 길어진다는 점이고, 그래서 topKInput을 설치 설정으로 둡니다.
리랭커를 빠져도 되는 단계로 두었습니다. 리랭커 장애가 검색 장애로 번지지 않는 대신, 폴백하는 동안 상위 5건의 품질(nDCG@5)은 위 실측에서 0.57에서 0.50 수준으로 말없이 내려갑니다. 그래서 폴백 여부와 이유를 결과와 로그에 남겨, 품질이 떨어진 검색을 나중에 찾을 수 있게 했습니다.
입력 형식은 모델 이름으로 가립니다. 확인한 Qwen3·bge 계열에는 맞지만, 새 계열의 리랭커를 붙이면 그 모델이 어떤 형식을 기대하는지 먼저 확인해야 합니다. 템플릿이 틀려도 호출은 성공하고 점수만 무의미해지므로, 실패 로그로는 드러나지 않습니다.
청크는 자르지 않고 그대로 리랭커에 보냅니다. 청크가 리랭커 모델의 입력 상한을 넘으면 그 호출은 실패하고 RRF 순서로 폴백하므로 검색은 끊기지 않습니다. 문서가 기본값인 4,000자 청크로 잘리는 설치에서 입력 상한이 작은 리랭커로 바꾼다면 청크 크기를 함께 줄여야 리랭킹이 실제로 돕니다.
메모리와 검색의 다른 글

엔지니어링 노트 · 메모리와 검색
개인 메모리는 매 턴 싣고, 고칠 때는 통째로 다시 쓴다
사용자가 「이슈 트래커에서 제 계정은 이 ID예요. 앞으로 내 이슈는 이걸로 찾아 주세요」라고 하면 모델은 「기억하겠습니다」라고 답합니다.

엔지니어링 노트 · 메모리와 검색
대화에서 바로잡은 컬럼 뜻, 테이블 메모로 다음 사람에게 싣기
고객 테이블의 상태 컬럼 STATCD 에는 1, 2, 9가 들어 있습니다. 스키마만 봐서는 9가 해지라는 것을 알 수 없어서, 「활성 고객 수」를 물으면 모델은 해지 고객까지 세어 보고서에 올립니다.

엔지니어링 노트 · 메모리와 검색
조사 붙은 검색어는 전문 검색에 안 걸린다, 낱말을 OR로 잇는다
테이블 메모에 「반품 주문은 ORDRTN 테이블에 따로 쌓인다」라고 적혀 있고, 사용자가 「지난달 반품 주문 건수를 알려 줘」라고 묻습니다.