대부분의 팀은 첫 번째 검색 시스템을 다음과 같은 방식으로 구축합니다. 모든 문서를 512개 토큰의 고정된 청크로 나누고, 이를 벡터 데이터베이스에 밀어 넣은 뒤, 임베딩 모델이 어려운 일을 다 해줄 것이라고 기대하는 방식입니다. 그런 기대는 데모 단계까지는 통할지 모르지만, 실제 사용자를 마주하는 순간 무너집니다.

프로덕션 환경에서는 책임 조항을 예외 조항으로부터 분리해 버리면 법률 계약서의 논리가 깨집니다. 코드 샘플이 함수 시그니처에서 떨어져 나가면 API 문서는 쓸모없어집니다. 고객 지원 스레드에서 단 하나의 불만 사항을 대화 기록에서 떼어내면 그것은 소음이 될 뿐입니다. 문제는 파이프라인 끝에 있는 언어 모델인 경우가 거의 없습니다. 진짜 문제는 모델에 무엇을 입력하느냐입니다.

우리는 이를 뼈아픈 경험을 통해 배웠습니다. 초기 검색 레이어는 표준적인 것처럼 보였지만 일관성 없게 동작했습니다. 그래서 우리는 '검색을 마법이 아닌, 정밀하게 측정된 인프라로 취급한다'는 단순한 아이디어를 바탕으로 시스템을 재구축했습니다. 여기에서 정확히 무엇이 바뀌었는지, 그리고 어떻게 재현율(recall)을 95%까지 끌어올리는 동시에 95백분위수 지연 시간(95th-percentile latency)을 850ms에서 320ms로 단축했는지 설명하겠습니다.

고정 청크의 함정

균일한 토큰 수는 코딩하기 쉽고 설명하기도 쉽습니다. 하지만 이러한 편리함은 하나의 근본적인 사실을 가립니다. 바로 문서에는 구조가 있다는 점입니다. 그 구조를 무시하면 신호(signal)를 파괴하게 됩니다.

10페이지 분량의 기본 서비스 계약서(MSA)를 예로 들어보겠습니다. 512개 토큰으로 고정하여 자르면 의무 사항 중간에서 끊기게 되어, 해당 조항을 제한하는 상한표(cap table)로부터 조항을 분리해 버릴 수 있습니다. 그러면 검색 단계에서는 생각의 절반만 반환하게 되고, 생성 모델은 나머지 절반을 환각(hallucinate)합니다. API 문서의 경우, 청크가 너무 크면 불필요한 헤더(boilerplate headers)로 인해 임베딩이 희석되어 개발자가 필요로 하는 특정 메서드가 묻혀버립니다. 고객 지원 티켓의 경우, 고정된 윈도우 방식은 대화를 단순히 문장들의 집합(bag of sentences)으로 취급하여, 무엇이 실제로 실패했는지를 보여주는 앞뒤 맥락을 제거해 버립니다.

우리는 청크 크기를 단순히 추측에 의존하는 하이퍼파라미터로 취급하는 것을 그만두었습니다. 대신 문서 유형과 그 내부의 정보 구조(information architecture) 사이의 매핑 작업으로 다루기 시작했습니다.

데이터에 맞춘 청킹 전략

해결책은 단 하나의 완벽한 청크 크기가 아닙니다. 세 가지 서로 다른 데이터 형태에 맞춰 조정된 세 가지 별도의 전략입니다.

법률 문서는 이제 재귀적 분할(recursive splitting)을 거칩니다. 알고리즘은 먼저 섹션, 하위 섹션, 번호가 매겨진 조항 등 가장 큰 자연적 경계를 찾고, 필요한 경우에만 더 작은 단위로 분할합니다. 이를 통해 종료 조항이 그 생존 조건과 함께 유지되도록 합니다. 검색 단계에서 완전한 논리적 단위를 확인하게 되므로, 모델이 누락된 예외 사항을 임의로 만들어내는 환각 현상을 급격히 줄일 수 있습니다.

API 및 코드 문서에는 구조 인식 청킹(structure-aware chunking)을 적용합니다. 마크다운 헤더, 코드 펜스(code fences), 파라미터 테이블을 원자적 단위(atomic units)로 파싱합니다. 코드 블록 내부에서는 분할하지 않으며, 독스트링(docstrings)을 시그니처 옆에 유지합니다. 그 결과, 특정 클래스 메서드에 대한 쿼리가 개발자에게 필요한 전체 컨텍스트(설명, 타입이 지정된 파라미터, 작동 예시)를 모두 가져올 수 있게 됩니다.

고객 지원 및 대화 데이터에는 의미론적 청킹(semantic chunking)을 사용합니다. 토큰 수를 세는 대신 주제나 의도의 변화를 살핍니다. 고객이 세 번째 메시지에서 버그를 설명하고 일곱 번째 메시지에서 스택 트레이스(stack trace)를 붙여넣는다면, 메시지 인덱스가 아닌 의미를 기준으로 청킹합니다. 그러면 검색 레이어는 고립된 문장이 아니라 문제의 전체 흐름을 반환합니다.

벡터 검색만으로는 부족한 이유

완벽한 청크라 할지라도 순수 벡터 검색에서는 한계가 있습니다. 밀집 임베딩(dense embeddings)은 의미와 유의어를 포착하는 데는 뛰어나지만, 정확한 문자열(exact strings)에 대해서는 매우 모호하다는 단점이 있습니다. 엔지니어가 정확한 에러 코드인 ERR_CONNECTION_REFUSED를 검색할 때, 벡터 유사도는 개념적으로 유사한 수십 개의 결과는 반환할 수 있지만, 14위쯤에 묻혀 있는 정확한 일치 항목은 놓칠 수 있습니다.

BM25를 이용한 키워드 검색은 정반대의 문제를 가집니다. 정확한 토큰은 찾아내지만 의미적 의도는 놓칩니다. "왜 내 데이터베이스가 다운되었나요?"라고 묻는 사용자는 "연결 시간 초과 문제 해결"이라고 적힌 문서와 결코 매칭되지 않을 것입니다.

이제 우리는 두 방식을 모두 사용합니다. 벡터 검색 결과와 키워드 검색 결과를 상호 순위 융합(Reciprocal Rank Fusion, RRF)에 입력하여, 별도의 점수 보정 없이 두 순위 목록을 혼합합니다. 이렇게 융합된 목록은 다시 크로스 인코더 리랭커(cross-encoder reranker)를 거칩니다. 리랭커는 초기 검색보다 느리지만, 압축된 임베딩을 통하는 대신 쿼리와 문서 간의 관련성을 직접 판단하기 때문에 훨씬 더 정밀합니다. 이 하이브리드 파이프라인만으로도 재현율을 15% 끌어올렸습니다.

인덱스에 도달하기 전 잘못된 쿼리 수정하기

사용자는 이상적인 검색 쿼리를 작성하지 않습니다. 잘린 로그 라인을 붙여넣거나, "고장 났어요"라고 입력하거나, 문서에는 없는 전문 용어를 사용합니다. 원본 쿼리를 그대로 믿는다면, 그것은 노이즈를 믿는 것과 같습니다.

이제 우리는 모든 입력 쿼리를 검색 레이어(retrieval layer)로 보내기 전에 3~5개의 변형으로 확장합니다. 한 변형은 직접적인 의역일 수 있고, 다른 하나는 가상의 이상적인 문서 제목일 수 있습니다. 세 번째는 대화형 미사여구를 제거하고 기술적 키워드만 추출한 형태입니다. 각 변형은 임베딩되어 검색됩니다. 그런 다음 후보 풀을 중복 제거하고 병합합니다.

이것이 공짜는 아닙니다. 추가적인 임베딩 호출은 비용이 들고 몇 밀리초의 시간이 더 소요됩니다. 하지만 재현율(recall)에 미치는 효과는 극적이었습니다. 검색 전 쿼리를 확장함으로써 78%에서 96%로 끌어올렸습니다. 더 나은 검색은 생성 윈도우(generation window)를 줄이고 모델이 정확한 컨텍스트에 기반하도록 만들기 때문에, 결과적으로 다운스트림 비용을 절감할 수 있었습니다. 약간 더 비용이 드는 검색 단계가 길고 환각(hallucination)이 발생하는 생성 단계보다 훨씬 경제적입니다.

추측을 멈추고, 검색을 시작하세요.

적절한 청킹(chunking), 하이브리드 검색, 쿼리 확장을 갖춘 후에도 우리는 여전히 조합의 복잡함이라는 문제에 직면했습니다. 청크 크기, 청크 중첩(overlap), top-k 검색 깊이, 리랭커(reranker) 컷오프, 퓨전 가중치 등이 모두 서로 영향을 미칩니다. 수동 그리드 서치(grid search)를 했다면 몇 주가 걸렸을 것이고, 여전히 지역 최적점(local maximum)에 머물렀을 것입니다.

우리는 이 공간을 탐색하기 위해 베이지안 최적화(Bayesian optimization)로 전환했습니다. 모든 조합을 철저하게 테스트하는 대신, 검색 알고리즘은 어떤 구성이 좋은 성능을 낼 가능성이 높은지에 대한 믿음(belief)을 유지하며 유망한 영역으로 점진적으로 좁혀 나갑니다.

결과물은 단 하나의 완벽한 설정이 아닙니다. 그것은 선택지들의 파레토 프런티어(Pareto frontier)입니다. 한쪽 끝에는 높은 처리량의 API 지원 엔드포인트에 최적화된 가벼운 구성이 있습니다. 빠른 추론, 적절한 재현율, 그리고 가능한 최저 지연 시간(latency)을 목표로 합니다. 다른 쪽 끝에는 법률 검토를 위한 공격적인 구성이 있습니다. 밀리초를 희생하더라도 철저함을 위해 더 깊은 검색, 더 무거운 리랭킹, 더 촘촘한 중첩을 사용합니다. 프런티어가 명확하기 때문에, 우리는 '모두에게 맞는 하나의 설정'을 고집하는 대신 제품에 맞는 적절한 지점을 선택할 수 있습니다.

실제 수치는 어떠한가

이러한 변화를 통해 시스템은 취약한 프로토타입에서 측정 가능한 프로덕션 파이프라인으로 진화했습니다.

Recall at ten이 78%에서 95%로 향상되었습니다. 이는 코퍼스(corpus)에 정답이 존재할 때, 20번 중 19번은 찾아낼 수 있음을 의미합니다.

95퍼센타일(95th percentile) 지연 시간이 850ms에서 320ms로 감소했습니다. 하이브리드 스택이 서류상으로는 더 무거워 보일 수 있지만, 더 스마트한 인덱싱, 더 작은 리랭커, 그리고 필요한 경우에만 공격적인 청크를 제공하는 기능 덕분에 전체 시스템이 더 빨라졌습니다.

환각률(Hallucination rate)—별도로 분리된 골든 데이터셋(golden dataset)을 통해 인간 어노테이터가 추적한 결과—이 12%에서 3%로 떨어졌습니다. 모델이 완전하고 관련성 있는 컨텍스트를 받으면, 사실을 지어내는 일을 멈춥니다.

쿼리당 비용이 $0.008에서 $0.005로 감소했습니다. 더 나은 검색은 더 짧고 집중된 LLM 프롬프트를 의미하며, 복구 시도 횟수를 줄여줍니다. 쿼리 확장에 들어가는 추가적인 임베딩 비용은 생성 단계에서 절감되는 비용에 비하면 미미한 수준입니다.

골든 데이터셋을 구축하고 검색을 코드처럼 다루세요

이 글에서 한 가지만 얻어 가신다면, 그것은 바로 '측정의 규율'이어야 합니다. 우리는 실제 질문과 검증된 답변 위치로 구성된 작은 골든 데이터셋을 구축했습니다. 어떤 변경 사항이 프로덕션에 적용되기 전에, 해당 데이터셋을 대상으로 테스트를 거칩니다. 재현율과 지연 시간은 노트북에서 눈대중으로 확인하는 것이 아니라 실시간으로 모니터링됩니다.

검색은 연구용 데모가 아닙니다. 인프라입니다. 검색은 스택의 나머지 부분과 마찬가지로 단위 테스트, 회귀 벤치마크, 자동화된 최적화가 필요합니다. 토큰에 대한 미신이 아니라 문서 구조에 따라 청킹하세요. 벡터 검색과 키워드 검색을 리랭커와 결합하세요. 사용자가 실제로 작성하는 쿼리를 확장하세요. 그런 다음 직관 대신 검색 알고리즘이 설정값(knobs)을 조정하게 하세요.

우리가 설명한 파이프라인은 이론적인 것이 아닙니다. 원문은 여기에서 읽어보실 수 있으며, 이 분야에 관심 있는 커뮤니티와 검색 엔지니어링에 대해 논의하고 싶다면 GyaanSetu AI group에 참여하세요.