설계기준 RAG의 하이브리드 검색 구현

건설 설계기준 검색에서 dense 검색만으로는 Hit@3 0.35에 머물렀다. kiwipiepy 형태소 BM25를 붙여 RRF로 합치고 교차 인코더 리랭커로 후보 20개를 재정렬해 0.90이 됐다. 질의 20개의 순위 원본과 RRF가 순위를 바꾸는 조건, 기호 질의에서 리랭커를 끄게 된 측정값을 함께 싣는다.

Seobway · · 14분

1. 배경

1.1 문제 상황

구조 계산서에는 판단의 근거가 된 설계기준을 적는다. 「KDS 11 80 05 4.4.1」처럼 조항 번호를 달거나, 설계기준 PDF의 식과 표를 쪽 번호와 함께 옮겨 적는다. 이 레퍼런스 인용 작업을 줄이려고 KDS 웹 원문과 설계기준 PDF를 한 번에 검색하는 RAG 검색을 만들기 시작했다. 질문을 넣으면 근거 조항을 조항 번호, 쪽 번호와 함께 돌려주고, 로컬 LLM이 그 조항을 인용해 답한다. 인용한 조항이 맞는지는 사람이 원문으로 확인하므로, 검색이 정답 조항을 상위에 올리는 것이 먼저다.

1.2 dense 검색의 실패 유형

처음 구현은 임베딩 벡터만 쓰는 dense 검색이었다. 조항 20개를 정답으로 지정한 평가셋으로 재 보니 정답 조항이 상위 3개 안에 든 비율(Hit@3)이 0.35였다.

임베딩 모델 크기 Hit@1 Hit@3 Hit@5 MRR
paraphrase-multilingual-MiniLM-L12-v2 0.22GB 0.10 0.25 0.35 0.187
paraphrase-multilingual-mpnet-base-v2 1.0GB 0.20 0.35 0.40 0.285

실패한 질의를 하나씩 열어 보면 두 종류였다.

  1. 「주동토압 수동토압 정지토압」, 「국부좌굴 폭두께비」처럼 전문용어를 그대로 친 질의가 상위 20개에도 들지 못했다. 임베딩은 문장 의미를 벡터 하나로 압축하므로, 용어가 정확히 일치하는지는 점수에 약하게만 반영된다.
  2. 정답 문서는 찾았지만 조항이 틀렸다. 「기준안전율」 질의가 4.4 안정 검토 대신 같은 기준의 4.7 점검 및 유지관리로 갔다. 또 1.5 기호의 정의, 1.2 적용 범위 같은 짧은 일반 조항이 무관한 질의 여러 개에서 1위를 차지했다.

첫째는 어휘 일치 문제라 키워드 검색(BM25)으로, 둘째는 순위 문제라 리랭커로 풀었다. 모델을 MiniLM에서 mpnet으로 키워 0.25가 0.35가 됐지만 실패 유형은 그대로였다.


2. 방법

2.1 검색 대상과 평가셋

검색 단위는 조항 하나다. KDS 웹 원문은 국가건설기준센터 OpenAPI에서 받아 조항 번호 단위로 자르고, PDF는 목차(북마크) 항목 단위로 자른다. 조항 한 행에는 원문, 임베딩 벡터, 조항 번호, 쪽 번호, 딸린 표가 함께 저장된다(LanceDB).

평가셋 eval_set.jsonl은 질의 20개와 정답 조항의 (doc_code, labels)로 이루어진다. KDS 11 80 05(옹벽), KDS 17 10 00(내진), KDS 41 30 10(강구조), KDS 24 10 11(교량)의 조항을 직접 보고 만들었다. 자연어 질의와 용어만 나열한 질의가 섞여 있다.

{"query": "주동토압 수동토압 정지토압", "doc_code": "KDS 11 80 05", "labels": ["1.7.3"], "note": "키워드형(dense 약점 측정)"}

2.2 평가 지표

지표는 검색 결과 안에서 정답 조항이 처음 나온 순위로 계산한다. 답변 문장이 아니라 검색만 따로 잰다.

MRR=1∣Q∣∑i=1∣Q∣1ranki\text{MRR} = \frac{1}{|Q|} \sum_{i=1}^{|Q|} \frac{1}{\text{rank}_i}

목표는 Hit@3 0.9 이상으로 잡았다. 화면에 상위 결과 몇 개를 펼쳐 보여 주므로 3위 안에 들면 사용자가 바로 확인할 수 있다.

2.3 전체 구조

%% desc: 질의 하나가 dense와 BM25 두 목록으로 검색되고, RRF로 합쳐진 뒤 리랭커를 거쳐 상위 k개가 된다
flowchart LR
    Q[질의] --> D["dense 검색<br/>mpnet 코사인 상위 20"]
    Q --> B["BM25 검색<br/>kiwipiepy 형태소 상위 20"]
    D --> F["RRF 합산<br/>k = 60"]
    B --> F
    F --> G{"질의에 한글이 있나"}
    G -- 있음 --> R["리랭커<br/>jina-reranker-v2 교차 인코더"]
    G -- 없음 --> K["BM25 순위 사용"]
    R --> T[상위 8개 조항]
    K --> T

코드는 civil_engine/rag/ 아래에 있다. store.py가 dense 검색, lexical.py가 BM25, hybrid.py가 합산과 분기, rerank.py가 리랭커를 맡는다. hybrid.search()의 use_bm25, use_rerank 인자로 단계를 하나씩 끄고 켤 수 있어서 같은 평가셋으로 각 단계의 기여를 따로 잰다.

모델은 모두 fastembed의 ONNX 런타임으로 돌린다. PyTorch 없이 CPU에서 동작하므로 GPU가 없는 업무용 PC에서도 로컬로 실행된다.

2.4 dense 검색

질의와 조항을 같은 임베딩 모델(paraphrase-multilingual-mpnet-base-v2, 768차원)로 각각 벡터로 바꾸고 코사인 유사도로 정렬한다. 질의와 문서를 따로 인코딩하는 이 방식을 bi-encoder라고 한다[7]. 조항 벡터는 색인할 때 한 번만 계산해 두므로 질의 시점에는 질의 벡터 하나만 계산하면 된다.

# store.py
rows = db.open_table(_TABLE).search(qv).metric("cosine").limit(k).to_list()
d["score"] = 1.0 - float(r.get("_distance", 1.0))  # cosine 유사도

표현이 달라도 뜻이 같으면 찾는다. 「옹벽이 밀리는 것」으로 물어도 「활동」 조항이 나온다. 반대로 문장 전체를 벡터 하나로 요약하므로, 질의에 들어간 특정 용어가 조항에 그대로 있는지는 구분하지 못한다.

2.5 BM25와 한국어 토큰화

BM25는 질의 토큰이 문서에 몇 번 나오는지(tf), 그 토큰이 코퍼스 전체에서 얼마나 드문지(idf), 문서 길이가 평균보다 긴지를 조합한 점수다[1].

score(D,Q)=∑qi∈QIDF(qi)⋅f(qi,D) (k1+1)f(qi,D)+k1(1−b+b⋅∣D∣avgdl)\text{score}(D, Q) = \sum_{q_i \in Q} \text{IDF}(q_i) \cdot \frac{f(q_i, D)\,(k_1 + 1)}{f(q_i, D) + k_1 \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}

f(qi,D)f(q_i, D)는 문서 DD 안의 토큰 빈도, ∣D∣|D|는 문서 토큰 수, avgdl은 평균 문서 길이다. rank_bm25의 BM25Okapi 기본값(k1=1.5k_1 = 1.5, b=0.75b = 0.75)을 그대로 쓴다[2]. 코퍼스가 조항 1,000여 개라 역색인 서버 없이 메모리에 올린다.

점수 계산보다 토큰화가 결과를 더 크게 좌우했다. 한국어는 「주동토압에」처럼 명사에 조사가 붙고 「다차로재하계수」처럼 명사가 이어 붙는다. 공백으로 자르면 「주동토압에」와 「주동토압」이 다른 토큰이 된다. 그래서 kiwipiepy 형태소 분석기로 자른다[3].

# lexical.py
def _morphs(text: str) -> list[str]:
    # 형태소 표면형. 1글자 조사·기호는 BM25에서 노이즈라 길이 2+만(숫자/영문 코드는 유지)
    toks = []
    for t in _kiwi.tokenize(text):
        f = t.form
        if len(f) >= 2 or f.isdigit() or f.isascii():
            toks.append(f)
    return toks

한 글자 형태소를 버리는 필터는 「의」, 「를」 같은 조사를 없애려고 넣었다. 이 필터가 나중에 ω 같은 한 글자 그리스 문자까지 버린다는 사실이 드러났고, 그 과정은 설계기준 PDF 색인과 수식 기호 검색 실험에서 다룬다.

색인 텍스트는 조항 제목과 본문을 합친 search_text다. 제목에 조항 번호와 조항 이름이 들어 있어서 「옹벽 활동」 같은 짧은 질의가 제목에서 바로 걸린다.

2.6 RRF로 두 순위 합치기

dense 점수는 코사인 유사도(0~1)이고 BM25 점수는 상한이 없는 양수다. 두 점수를 더하려면 정규화 방식과 가중치를 정해야 한다. RRF(Reciprocal Rank Fusion)는 점수를 버리고 순위만 쓴다[4].

RRF(d)=∑r∈R1k+rankr(d)\text{RRF}(d) = \sum_{r \in R} \frac{1}{k + \text{rank}_r(d)}

RR은 합칠 순위 목록들, rankr(d)\text{rank}_r(d)는 목록 rr에서 문서 dd의 순위(1부터)다. 목록에 없는 문서는 그 목록에서 0점을 받는다. k=60k = 60은 원 논문의 값이다.

# hybrid.py
_RRF_K = 60

def _rrf(rankings: list[list[str]], k: int = _RRF_K) -> dict[str, float]:
    """Reciprocal Rank Fusion: 각 순위에서 1/(k+rank) 합산."""
    scores: dict[str, float] = {}
    for ranking in rankings:
        for rank_pos, _id in enumerate(ranking, 1):
            scores[_id] = scores.get(_id, 0.0) + 1.0 / (k + rank_pos)
    return scores

k=60k = 60에 목록마다 후보 20개를 쓰면 점수 범위가 이렇게 나뉜다.

경우 최고 점수 최저 점수
한 목록에만 있음 1/61 = 0.0164 (1위) 1/80 = 0.0125 (20위)
두 목록에 모두 있음 2/61 = 0.0328 2/80 = 0.0250

두 목록에 모두 든 문서의 최저 점수(0.0250)가 한 목록에만 든 문서의 최고 점수(0.0164)보다 크다. 그래서 이 설정의 RRF는 두 검색기가 모두 찾은 문서를 먼저 세우고, 그 안에서 순위 합으로 정렬한다. 한쪽에서 1위를 해도 다른 쪽 상위 20개에 없으면 두 목록에 모두 든 문서들 뒤로 밀린다.

이 성질은 아래 질의별 결과에서 그대로 보인다.

질의 dense 순위 BM25 순위 RRF 순위
옹벽 기초지반 지지력 검토 3 14 3
주동토압 수동토압 정지토압 20위 밖 1 3
깎기 경계구간에서의 토압 산정 방법 20위 밖 9 18

첫 질의는 두 목록에 모두 있어서 BM25 14위가 3위로 올라왔다. 둘째와 셋째 질의는 정답이 BM25 목록에만 있어서, 두 목록에 모두 든 오답들 뒤로 밀렸다. 전문용어 질의에서는 합산이 BM25 단독보다 나빠질 수 있다는 뜻이다. 이 손실은 다음 단계의 리랭커가 복구한다.

2.7 교차 인코더 리랭커

리랭커는 질의와 조항을 한 입력으로 이어 붙여 트랜스포머에 넣고, 관련도 점수 하나를 출력한다. 이 방식을 cross-encoder라고 한다[5]. 질의 토큰과 조항 토큰이 모든 층에서 어텐션으로 직접 비교되므로 bi-encoder보다 정확하다. 대신 조항 벡터를 미리 계산해 둘 수 없어서 후보마다 모델을 한 번씩 실행해야 한다.

# rerank.py
_RERANK_MODEL = "jinaai/jina-reranker-v2-base-multilingual"

def rerank(query: str, rows: list[dict[str, Any]], top_k: int) -> list[dict[str, Any]]:
    scores = list(_model().rerank(query, [r.get("text", "") for r in rows]))
    order = sorted(range(len(rows)), key=lambda i: scores[i], reverse=True)[:top_k]
    ...
    # 리랭커는 logit(음수 가능) → 표시용 0~1 정규화(순서 보존)
    r["score"] = 1.0 / (1.0 + math.exp(-float(scores[i])))

모델 출력은 범위 제한이 없는 logit이라 화면 표시용으로만 시그모이드를 씌워 0~1로 바꾼다. 순서는 바뀌지 않는다.

모델은 다국어 학습이 명시된 jina-reranker-v2-base-multilingual을 골랐다[6]. RRF 상위 20개를 넘기고 상위 8개를 받는다. CPU에서 질의 하나당 약 9초가 걸린다. 후보를 20개보다 늘리면 이 시간이 비례해서 늘어난다.


3. 결과

3.1 도입 시점 측정

처음 도입할 때(KDS 4개 기준의 웹 조항만 색인한 상태) 같은 20문항으로 잰 결과다.

구성 Hit@1 Hit@3 Hit@5 MRR
dense 0.20 0.35 0.40 0.285
dense + BM25 (RRF) 0.30 0.60 0.75 0.463
dense + BM25 + 리랭커 0.75 0.90 0.95 0.829

3.2 코퍼스 확장 후 측정

이후 도로교 설계기준 PDF 조항 481개와 KDS 웹 조항 806개를 합친 1,287개로 코퍼스가 커진 상태에서, 같은 20문항을 방식별로 다시 쟀다. 질의별 순위가 모두 남아 있는 측정이다.

구성 Hit@1 Hit@3 Hit@5 MRR
dense 0.15 0.30 0.30 0.225
BM25 0.40 0.60 0.75 0.535
dense + BM25 (RRF) 0.25 0.65 0.75 0.450
dense + BM25 + 리랭커 0.75 0.90 0.95 0.838

검색 대상이 네 배 가까이 늘었는데 최종 Hit@3은 0.90으로 같았다.

3.3 질의별 순위 분석

1위2~3위4~20위20위 밖denseBM25RRF+리랭커옹벽에 작용하는 토압의 종류와 어떤 토압이론으로 계산하는가1. 옹벽에 작용하는 토압의 종류와 어떤 토압이론으…1. 옹벽에 작용하는 토압의 종류와 어떤 토압이론으로 계산하는가 · dense: 2위21. 옹벽에 작용하는 토압의 종류와 어떤 토압이론으로 계산하는가 · BM25: 1위11. 옹벽에 작용하는 토압의 종류와 어떤 토압이론으로 계산하는가 · RRF: 1위11. 옹벽에 작용하는 토압의 종류와 어떤 토압이론으로 계산하는가 · +리랭커: 1위1옹벽 배면 상재하중에 의한 토압은 어떻게 고려하나2. 옹벽 배면 상재하중에 의한 토압은 어떻게 고려…2. 옹벽 배면 상재하중에 의한 토압은 어떻게 고려하나 · dense: 15위152. 옹벽 배면 상재하중에 의한 토압은 어떻게 고려하나 · BM25: 3위32. 옹벽 배면 상재하중에 의한 토압은 어떻게 고려하나 · RRF: 3위32. 옹벽 배면 상재하중에 의한 토압은 어떻게 고려하나 · +리랭커: 1위1깎기 경계구간에서의 토압 산정 방법3. 깎기 경계구간에서의 토압 산정 방법3. 깎기 경계구간에서의 토압 산정 방법 · dense: 20위 밖밖3. 깎기 경계구간에서의 토압 산정 방법 · BM25: 9위93. 깎기 경계구간에서의 토압 산정 방법 · RRF: 18위183. 깎기 경계구간에서의 토압 산정 방법 · +리랭커: 1위1옹벽 활동에 대한 안정성 검토4. 옹벽 활동에 대한 안정성 검토4. 옹벽 활동에 대한 안정성 검토 · dense: 3위34. 옹벽 활동에 대한 안정성 검토 · BM25: 1위14. 옹벽 활동에 대한 안정성 검토 · RRF: 1위14. 옹벽 활동에 대한 안정성 검토 · +리랭커: 1위1옹벽 전도에 대한 안정성은 어떻게 검토하는가5. 옹벽 전도에 대한 안정성은 어떻게 검토하는가5. 옹벽 전도에 대한 안정성은 어떻게 검토하는가 · dense: 6위65. 옹벽 전도에 대한 안정성은 어떻게 검토하는가 · BM25: 5위55. 옹벽 전도에 대한 안정성은 어떻게 검토하는가 · RRF: 2위25. 옹벽 전도에 대한 안정성은 어떻게 검토하는가 · +리랭커: 1위1옹벽 기초지반 지지력 검토6. 옹벽 기초지반 지지력 검토6. 옹벽 기초지반 지지력 검토 · dense: 3위36. 옹벽 기초지반 지지력 검토 · BM25: 14위146. 옹벽 기초지반 지지력 검토 · RRF: 3위36. 옹벽 기초지반 지지력 검토 · +리랭커: 2위2옹벽 안정해석 시 적용하는 기준안전율7. 옹벽 안정해석 시 적용하는 기준안전율7. 옹벽 안정해석 시 적용하는 기준안전율 · dense: 20위 밖밖7. 옹벽 안정해석 시 적용하는 기준안전율 · BM25: 2위27. 옹벽 안정해석 시 적용하는 기준안전율 · RRF: 5위57. 옹벽 안정해석 시 적용하는 기준안전율 · +리랭커: 1위1연약지반 위에 옹벽을 설치할 때 고려사항8. 연약지반 위에 옹벽을 설치할 때 고려사항8. 연약지반 위에 옹벽을 설치할 때 고려사항 · dense: 1위18. 연약지반 위에 옹벽을 설치할 때 고려사항 · BM25: 1위18. 연약지반 위에 옹벽을 설치할 때 고려사항 · RRF: 1위18. 연약지반 위에 옹벽을 설치할 때 고려사항 · +리랭커: 1위1옹벽 배면 배수공 설계9. 옹벽 배면 배수공 설계9. 옹벽 배면 배수공 설계 · dense: 1위19. 옹벽 배면 배수공 설계 · BM25: 5위59. 옹벽 배면 배수공 설계 · RRF: 1위19. 옹벽 배면 배수공 설계 · +리랭커: 1위1주동토압 수동토압 정지토압10. 주동토압 수동토압 정지토압10. 주동토압 수동토압 정지토압 · dense: 20위 밖밖10. 주동토압 수동토압 정지토압 · BM25: 1위110. 주동토압 수동토압 정지토압 · RRF: 3위310. 주동토압 수동토압 정지토압 · +리랭커: 1위1활동 전도 지지력 안전율 검토 항목11. 활동 전도 지지력 안전율 검토 항목11. 활동 전도 지지력 안전율 검토 항목 · dense: 20위 밖밖11. 활동 전도 지지력 안전율 검토 항목 · BM25: 1위111. 활동 전도 지지력 안전율 검토 항목 · RRF: 2위211. 활동 전도 지지력 안전율 검토 항목 · +리랭커: 1위1시설물 내진등급은 어떻게 분류하는가12. 시설물 내진등급은 어떻게 분류하는가12. 시설물 내진등급은 어떻게 분류하는가 · dense: 20위 밖밖12. 시설물 내진등급은 어떻게 분류하는가 · BM25: 1위112. 시설물 내진등급은 어떻게 분류하는가 · RRF: 2위212. 시설물 내진등급은 어떻게 분류하는가 · +리랭커: 1위1내진성능수준의 종류와 정의13. 내진성능수준의 종류와 정의13. 내진성능수준의 종류와 정의 · dense: 20위 밖밖13. 내진성능수준의 종류와 정의 · BM25: 3위313. 내진성능수준의 종류와 정의 · RRF: 6위613. 내진성능수준의 종류와 정의 · +리랭커: 1위1설계지반운동 수준은 어떻게 분류하나14. 설계지반운동 수준은 어떻게 분류하나14. 설계지반운동 수준은 어떻게 분류하나 · dense: 20위 밖밖14. 설계지반운동 수준은 어떻게 분류하나 · BM25: 1위114. 설계지반운동 수준은 어떻게 분류하나 · RRF: 3위314. 설계지반운동 수준은 어떻게 분류하나 · +리랭커: 1위1지반 액상화 검토15. 지반 액상화 검토15. 지반 액상화 검토 · dense: 20위 밖밖15. 지반 액상화 검토 · BM25: 4위415. 지반 액상화 검토 · RRF: 10위1015. 지반 액상화 검토 · +리랭커: 2위2성능기반 내진설계 해석방법16. 성능기반 내진설계 해석방법16. 성능기반 내진설계 해석방법 · dense: 1위116. 성능기반 내진설계 해석방법 · BM25: 2위216. 성능기반 내진설계 해석방법 · RRF: 1위116. 성능기반 내진설계 해석방법 · +리랭커: 1위1강구조 사용하중에 의한 처짐 사용성 기준17. 강구조 사용하중에 의한 처짐 사용성 기준17. 강구조 사용하중에 의한 처짐 사용성 기준 · dense: 20위 밖밖17. 강구조 사용하중에 의한 처짐 사용성 기준 · BM25: 10위1017. 강구조 사용하중에 의한 처짐 사용성 기준 · RRF: 18위1817. 강구조 사용하중에 의한 처짐 사용성 기준 · +리랭커: 1위1휨과 압축 조합력에서 국부좌굴 폭두께비 검토18. 휨과 압축 조합력에서 국부좌굴 폭두께비 검토18. 휨과 압축 조합력에서 국부좌굴 폭두께비 검토 · dense: 20위 밖밖18. 휨과 압축 조합력에서 국부좌굴 폭두께비 검토 · BM25: 20위 밖밖18. 휨과 압축 조합력에서 국부좌굴 폭두께비 검토 · RRF: 20위 밖밖18. 휨과 압축 조합력에서 국부좌굴 폭두께비 검토 · +리랭커: 20위 밖밖교량 설계기준의 적용범위19. 교량 설계기준의 적용범위19. 교량 설계기준의 적용범위 · dense: 10위1019. 교량 설계기준의 적용범위 · BM25: 10위1019. 교량 설계기준의 적용범위 · RRF: 4위419. 교량 설계기준의 적용범위 · +리랭커: 4위4슬래브교에 대한 등가 스트립 폭20. 슬래브교에 대한 등가 스트립 폭20. 슬래브교에 대한 등가 스트립 폭 · dense: 20위 밖밖20. 슬래브교에 대한 등가 스트립 폭 · BM25: 1위120. 슬래브교에 대한 등가 스트립 폭 · RRF: 3위320. 슬래브교에 대한 등가 스트립 폭 · +리랭커: 2위2
그림 1. KDS 질의 20개의 정답 순위, 검색 단계별. 칸의 숫자는 정답이 처음 나온 순위다. RRF 열에서 BM25에만 걸린 정답의 순위가 내려가고, 리랭커 열에서 대부분 1위로 올라온다. 데이터: exp_result4.json

같은 값을 표로 옮기면 다음과 같다. 0은 상위 20개 밖이다.

# 질의 정답 조항 dense BM25 RRF +리랭커
1 옹벽에 작용하는 토압의 종류와 어떤 토압이론으로 계산하는가 KDS 11 80 05 1.7.3 2 1 1 1
2 옹벽 배면 상재하중에 의한 토압은 어떻게 고려하나 KDS 11 80 05 1.7.4 15 3 3 1
3 깎기 경계구간에서의 토압 산정 방법 KDS 11 80 05 1.7.7 0 9 18 1
4 옹벽 활동에 대한 안정성 검토 KDS 11 80 05 4.4.1 3 1 1 1
5 옹벽 전도에 대한 안정성은 어떻게 검토하는가 KDS 11 80 05 4.4.3 6 5 2 1
6 옹벽 기초지반 지지력 검토 KDS 11 80 05 4.4.4 3 14 3 2
7 옹벽 안정해석 시 적용하는 기준안전율 KDS 11 80 05 4.4 0 2 5 1
8 연약지반 위에 옹벽을 설치할 때 고려사항 KDS 11 80 05 4.3.2 1 1 1 1
9 옹벽 배면 배수공 설계 KDS 11 80 05 4.6.3 1 5 1 1
10 주동토압 수동토압 정지토압 KDS 11 80 05 1.7.3 0 1 3 1
11 활동 전도 지지력 안전율 검토 항목 KDS 11 80 05 4.1.1, 4.4 0 1 2 1
12 시설물 내진등급은 어떻게 분류하는가 KDS 17 10 00 4.1.1 0 1 2 1
13 내진성능수준의 종류와 정의 KDS 17 10 00 4.1.2 0 3 6 1
14 설계지반운동 수준은 어떻게 분류하나 KDS 17 10 00 4.1.3 0 1 3 1
15 지반 액상화 검토 KDS 17 10 00 4.7 0 4 10 2
16 성능기반 내진설계 해석방법 KDS 17 10 00 4.4.3 1 2 1 1
17 강구조 사용하중에 의한 처짐 사용성 기준 KDS 41 30 10 4.11.3 0 10 18 1
18 휨과 압축 조합력에서 국부좌굴 폭두께비 검토 KDS 41 30 10 4.15.4 0 0 0 0
19 교량 설계기준의 적용범위 KDS 24 10 11 1.1 10 10 4 4
20 슬래브교에 대한 등가 스트립 폭 KDS 24 10 11 4.6.4 0 1 3 2

표에서 읽히는 것은 다음과 같다.

3.4 기호 질의 예외 처리

설계기준 PDF를 색인에 넣은 뒤 fyt, φs처럼 수식 기호만으로 검색하는 질의를 따로 모아 쟀다(질의 150개, 리랭커는 30개 표본). 한글 문장 질의와 결과가 반대였다.

기호 질의 Hit@3 dense BM25 RRF +리랭커
수식 기호 질의 (A 유형) 0.027 0.833 0.627 0.533
E 한글 문장 질의denseE 한글 문장 질의 · dense: 0.3000.300BM25E 한글 문장 질의 · BM25: 0.6000.600RRFE 한글 문장 질의 · RRF: 0.6500.650+리랭커E 한글 문장 질의 · +리랭커: 0.9000.900A 수식 기호 질의denseA 수식 기호 질의 · dense: 0.0270.027BM25A 수식 기호 질의 · BM25: 0.8330.833RRFA 수식 기호 질의 · RRF: 0.6270.627+리랭커A 수식 기호 질의 · +리랭커: 0.5330.533
그림 2. 한글 문장 질의(E)와 수식 기호 질의(A)의 검색 방식별 Hit@3. 두 유형에서 단계별 추세가 반대다. 리랭커는 E 20개 전부, A 표본 30개. 데이터: exp_result4.json

BM25 단독이 가장 높고, 단계를 더할수록 떨어진다. RRF 뒤 후보 20개 안에 정답이 있는 비율은 0.967이었다. 정답은 리랭커에 들어갔는데 리랭커가 순위를 내렸다. 한 단어짜리 기호 질의는 리랭커가 질의와 조항의 관계를 판단할 문맥이 거의 없다.

그래서 한글이 한 글자도 없는 질의는 리랭커를 건너뛰고 BM25 순위를 그대로 앞에 둔다.

# hybrid.py
_HANGUL = re.compile(r"[가-힣]")

# 기호 질의(한글 없음, 예 「fyt」 「φs」)는 리랭커를 건너뛰고 BM25 순위를 쓴다
if use_bm25 and bm and not _HANGUL.search(query):
    use_rerank = False
    bm_ids = [row["id"] for row, _ in bm]
    seen = set(bm_ids)
    fused_ids = bm_ids + [i for i in fused_ids if i not in seen]

기호 질의 Hit@3은 0.533에서 0.767이 됐다. 한글 질의는 이 분기를 타지 않으므로 앞의 20문항 결과(0.90)는 바뀌지 않는다. 판별 규칙을 「한글 포함 여부」 하나로 둔 이유도 이것이다. 기존 질의에 영향이 없다는 것을 코드만 보고 확인할 수 있다.

기호 질의가 애초에 BM25에서 0.833이 나오기까지는 PDF에 PUA 코드로 저장된 수식 글자를 복원하는 작업이 필요했다. 복원 전 기호 질의 Hit@3은 0.013이었다. 그 과정과 표 색인 실험은 설계기준 PDF 색인과 수식 기호 검색 실험에 정리했다.


4. 논의

4.1 검토한 대안

대안 판단
dense 모델을 multilingual-e5-large(2.24GB)로 교체 MiniLM에서 mpnet으로 키웠을 때 0.25가 0.35가 됐다. 실패 원인이 어휘 일치여서 모델 크기로는 절반만 해결된다고 봤다. 학습 도메인 밖 문서에서 BM25가 여러 dense 모델보다 강하다는 벤치마크 결과도 있다[8]. BM25와 리랭커로 0.90에 도달해 교체하지 않았다.
LanceDB 내장 전문 검색(Tantivy) 한국어 토큰화가 약하다고 판단해 kiwipiepy 형태소 분석을 직접 붙였다. 두 방식을 같은 평가셋으로 비교하지는 않았다.
점수 가중합 (예: 0.7 × dense + 0.3 × BM25) 두 점수의 분포가 다르고 질의마다 BM25 점수 크기가 달라 가중치를 고정하기 어렵다. RRF는 튜닝할 값이 kk 하나다.
BM25 + RRF까지만 사용 Hit@3 0.60 ~ 0.65로 목표 미달이다.
bge-reranker-base 크기(1.04GB)가 비슷하다. 다국어 학습을 명시한 jina를 먼저 채택했고 A/B 측정은 하지 않았다.

4.2 한계


5. 결론


부록 A. 원시 데이터

본문의 1,287개 코퍼스 측정값은 모두 아래 파일에서 나왔다. 처음 도입 시점(KDS 4개 기준) 측정은 결과를 화면에만 출력해서 표만 남아 있다.

측정 조건은 임베딩 paraphrase-multilingual-mpnet-base-v2, 리랭커 jina-reranker-v2-base-multilingual, 후보 20개, RRF k=60k = 60이다.

참고

  1. [1] Robertson, Zaragoza — The Probabilistic Relevance Framework: BM25 and Beyond, Foundations and Trends in IR 2009
  2. [2] rank_bm25 — GitHub
  3. [3] kiwipiepy — GitHub
  4. [4] Cormack, Clarke, Büttcher — Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods, SIGIR 2009
  5. [5] Nogueira, Cho — Passage Re-ranking with BERT, arXiv 2019
  6. [6] jina-reranker-v2-base-multilingual — Hugging Face
  7. [7] Karpukhin et al. — Dense Passage Retrieval for Open-Domain Question Answering, EMNLP 2020
  8. [8] Thakur et al. — BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models, NeurIPS 2021

관련 글