Field Log · Entry

Retrieval 성능의 한계에 부딪쳤을 때 1: SPLADE보다 먼저 봐야 할 것

도메인 retrieval stage trace 표와 dense sparse fusion rerank 진단 흐름을 보여주는 오픈 그래프 카드

도메인 검색 시스템을 만들면서 처음 마주한 문제는 단순했다. 검색 결과가 기대만큼 나오지 않았다.

처음에는 비교적 명확한 방법들로 개선을 시도했다. 장비명, 부품명, 약어, 문서 제목, 파일명, 자주 쓰이는 현장 표현을 색인 텍스트에 추가했다. REPReplacement, BaratronPressure Gauge, Flow SwitchFlow S/W처럼 같은 의미로 쓰이는 표현들을 alias로 묶었다. 특정 필드에 가중치를 주거나, sparse 검색 결과를 후보군에 더 오래 남기는 방식도 적용했다.

이런 방식은 분명 효과가 있었다. 실제로 몇몇 실패 케이스는 규칙 몇 줄만으로 바로 회복되었다. 문제는 그 다음이었다.

새로운 실패 케이스가 나올 때마다 또 다른 alias를 추가하고, 또 다른 예외 규칙을 넣어야 했다. 어느 순간부터는 성능 개선보다 규칙 관리 비용이 더 커졌다. 검색이 왜 실패했는지 설명하기도 어려워졌고, 특정 케이스를 고치면 다른 케이스가 흔들리는 일도 생겼다.

결국 나는 같은 질문으로 돌아왔다.

이 문제를 계속 hard coding으로 풀 수 있을까?

이 글은 그 질문에서 시작한다. retrieval 성능이 일정 수준에서 더 이상 개선되지 않을 때, 특히 도메인 문서 검색에서 sparse, dense, hybrid, reranker를 모두 써도 한계가 보일 때 무엇을 먼저 봐야 하는지 정리해보려 한다.

첫 번째 글에서는 곧바로 SPLADE나 모델 학습으로 넘어가지 않는다. 그 전에 먼저 해야 할 일이 있다. 검색 결과가 실패했을 때 gold 문서가 dense 단계에서 없었는지, sparse 단계에서 밀렸는지, fusion에서 사라졌는지, 아니면 reranker가 낮게 평가했는지를 분리해서 보는 것이다.

모델을 바꾸기 전에 실패 지점을 알아야 한다. retrieval 문제를 “성능이 낮다”는 하나의 문장으로 뭉개면, 원인이 sparse인지, dense인지, fusion인지, reranker인지 알 수 없다. 그리고 원인을 모르면 더 큰 모델을 붙여도 같은 실패를 반복할 가능성이 높다.

이 글이 답하는 질문

  • 도메인 retrieval 개선이 hard coding 단계에서 왜 막히는가?
  • SPLADE 문헌 조사가 왜 필요했지만, 왜 바로 SPLADE 학습으로 가면 안 되는가?
  • hybrid retrieval 결과와 rerank 결과를 어떻게 stage별로 해석해야 하는가?
  • 지금 당장 할 일은 모델 학습인가, trace·alias·pool 보존인가?

왜 다른 방법을 찾게 되었나

도메인 검색에서 단순 BM25나 dense retrieval이 부딪히는 문제는 생각보다 구체적이다. 사용자는 REP라고 검색하는데 문서에는 Replacement라고 쓰여 있다. 사용자는 Baratron을 찾는데 문서 제목에는 Pressure GaugeManometer가 들어 있다. Flow Switch, Flow S/W, F-S가 같은 부품을 가리키기도 한다.

이런 문제는 처음에는 규칙으로 풀 수 있다.

REP:
  - Replacement
  - 교체

Baratron:
  - Pressure Gauge
  - Manometer
  - 압력 게이지

Flow Switch:
  - Flow S/W
  - F-S

문서 색인 텍스트도 보강할 수 있다.

original title
+ source pdf name
+ equipment
+ component
+ action
+ alarm
+ approved aliases
+ field expressions

여기까지는 좋은 방향이다. 문제는 규칙이 늘어날수록 시스템이 점점 설명하기 어려워진다는 점이다. Flow Switch를 고치기 위해 넣은 규칙이 다른 Switch 문서를 끌어올릴 수 있고, Pressure Gauge를 보강하다가 Pirani GaugeManometer와 경계가 흐려질 수 있다.

그래서 다른 접근을 찾기 시작했다. 그 과정에서 만난 것이 SPLADE와 Inference-Free SPLADE였다.

SPLADE가 매력적으로 보인 이유

BM25의 약점은 명확하다. 같은 의미라도 표면형이 다르면 다른 토큰으로 본다. 스파게티파스타, REPReplacement, BaratronPressure Gauge는 BM25 입장에서는 기본적으로 다른 말이다.

SPLADE는 이 문제를 learned sparse retrieval로 다룬다. BERT 계열 모델의 MLM head를 이용해 입력 문맥에서 중요한 vocabulary term을 활성화하고, 그 결과를 sparse vector로 만든다. dense retrieval처럼 의미를 보되, 결과는 inverted index와 잘 맞는 sparse 표현으로 남긴다. SPLADE v2 논문은 pooling 개선, document-only expansion, distillation 학습까지 다룬다.

한국어 SPLADE 학습기를 다룬 Youngjoon Jang님의 글에서도 핵심은 비슷하다. query-positive-negatives 데이터를 만들고, hard negative를 모으고, reranker가 매긴 score를 distillation label로 사용한다.

여기서 말하는 distillation score는 특별한 마법이 아니다. 강한 teacher reranker가 query-document 쌍에 매긴 관련도 점수다.

{
  "query": "Recipe Body Unmatch 원인과 조치",
  "docs": [
    "Recipe Body Unmatch TSG",
    "Recipe Download SOP",
    "다른 alarm TSG"
  ],
  "teacher_scores": [8.4, 2.1, -1.7]
}

student인 SPLADE는 이 점수 자체, 혹은 점수 차이, 혹은 후보 문서 전체에 대한 분포를 따라 배우게 된다. 그러면 단순히 “positive는 정답, negative는 오답”만 배우는 것이 아니라, “이 negative는 완전 오답은 아니지만 positive보다는 낮다” 같은 순위 감각까지 배운다.

Inference-Free SPLADE도 매력적이었다. query는 무거운 neural encoder 없이 tokenize 또는 IDF 기반으로 처리하고, document만 미리 확장해 색인하면 검색 시 neural inference 비용을 크게 줄일 수 있기 때문이다. Amazon/OpenSearch 계열의 inference-free sparse retriever 연구도 IDF-aware FLOPS와 distillation을 통해 relevance와 latency 사이의 균형을 다룬다(arXiv:2411.04403).

하지만 이 문헌 조사를 하면서 오히려 더 분명해진 점이 있었다.

SPLADE를 학습하려면 먼저 무엇이 positive이고, 무엇이 hard negative인지 알아야 한다.

즉, 지금 내 검색 시스템이 어디서 실패하는지 모르면 SPLADE 학습 데이터도 제대로 만들 수 없다.

최종 gold_rank만 보면 속는다

현재 검색 파이프라인은 대략 이런 구조다.

query
  -> dense retrieval
  -> sparse retrieval
  -> hybrid fusion
  -> cross-encoder rerank
  -> final top-k

여기서 최종 gold_rank만 보면 판단을 잘못할 수 있다.

예를 들어 최종 순위에서 gold 문서가 30위라면 “retriever가 약하다”고 말하기 쉽다. 하지만 실제 원인은 다를 수 있다.

케이스densesparsefusionrerank해석
A412081sparse는 약하지만 dense와 reranker가 살림
B1115retriever는 맞췄고 reranker가 밀어냄
C80561dense는 약하지만 sparse exact match가 살림
D----색인, alias, chunk, qrels부터 봐야 함

같은 최종 실패라도 처방이 완전히 다르다. sparse가 문제인 케이스에 reranker를 고치면 안 되고, reranker가 demote한 케이스에 SPLADE를 학습하면 안 된다.

그래서 먼저 retrieval stage trace를 만들었다.

SOP79 stage trace 결과

아래 결과는 SOP79 retrieval-only run에서 LLM 답변 생성을 제외하고, gold 문서의 stage별 rank를 추적한 것이다. 사용한 컬럼은 다음과 같다.

컬럼의미
densedense retrieval에서 gold 문서가 나온 순위
sparsesparse retrieval에서 gold 문서가 나온 순위
fuseddense/sparse fusion 이후 gold 문서 순위
rerankcross-encoder rerank 이후 gold 문서 순위
gold_rank최종 결과 기준 gold 문서 순위

제공된 표는 Top5 Hits만 모은 trace다. 즉 최종 top5 안에 들어온 74개 케이스를 보여준다. 전체 SOP79가 79개라면, 이 표에 없는 4, 21, 40, 42, 66 다섯 개가 먼저 봐야 할 진짜 miss다.

74개 hit만 놓고 stage별 수치를 요약하면 이렇다.

stagetop1top5top10top50median rank
dense13/7427/7438/7469/7410
sparse16/7126/7130/7133/7176
fused16/7428/7436/7465/7412
rerank55/7474/7474/7474/741

이 표만 봐도 몇 가지가 분명하다.

첫째, 최종 성능은 꽤 좋아 보인다. 74개 hit 기준으로 rerank 이후 top5는 전부 성공했고, top1도 55개다. 전체 79개 기준으로 보면 top5 hit는 74/79, 약 93.7%다.

둘째, sparse retrieval은 안정적으로 gold를 끌어오고 있지 않다. sparse median rank가 76이다. sparse top50 안에 gold가 들어온 케이스도 71개 중 33개뿐이다. 이건 “sparse가 잘하고 있고 reranker가 조금 정리했다”는 그림이 아니다.

셋째, reranker가 first-stage retrieval의 약점을 크게 덮고 있다. sparse rank가 100보다 큰데 rerank가 top1로 회복한 케이스가 23개였다.

예를 들면 이런 케이스들이다.

idxdensesparsefusedrerankgold_doc
269120161pm_flow switch.pdf
3164119791pm_manometer.pdf
388772691pm_pirani gauge.pdf
5335-711pm_prism source.pdf
756292722tm_slot vv housing o-ring.pdf

이 결과는 꽤 중요하다. #26 Flow Switch만 보면 sparse 120위, fused 16위인데 rerank가 1위로 살렸다. 만약 rerank pool을 조금만 좁혔다면 이 케이스는 실패했을 수 있다. #31, #38, #53, #75는 더 위험하다. fused rank가 69, 71, 72, 79까지 내려가는데도 reranker가 살렸다.

즉 현재 시스템은 “first-stage가 충분히 잘 찾고 reranker가 미세 조정하는 구조”가 아니다.

넓은 후보 pool 안에 gold가 겨우 살아 있고, reranker가 그걸 강하게 회복하는 구조다.

이 결과가 말해주는 것

처음에는 sparse 개선, SPLADE, query expansion을 먼저 떠올렸다. 그런데 trace를 보면 우선순위가 조금 달라진다.

1. sparse/search_text는 분명히 개선 대상이다

sparse median rank가 76이라는 건 가볍게 넘길 수 없다. 특히 부품명, 약어, action 표현이 중요한 SOP 검색에서 sparse가 이 정도로 밀린다면 alias와 search_text 보강은 계속 필요하다.

대표적으로 봐야 할 표현은 이런 것들이다.

표현
actionREP, Replacement, 교체
actionADJ, Adjustment, 조정
componentBaratron, Pressure Gauge, Manometer
componentFlow Switch, Flow S/W, F-S
documentsource_pdf, title, section heading

다만 여기서 “그러면 바로 SPLADE fine-tuning”으로 가면 안 된다. 현재 sparse가 약한 이유가 모델 표현력 때문인지, 색인 텍스트 누락 때문인지, query expansion 부재 때문인지 먼저 나눠야 한다.

2. rerank pool을 줄이면 성능이 무너질 수 있다

fused rank가 50 밖인데 rerank가 살린 케이스가 9개 있었다. 이 말은 운영 최적화 과정에서 rerank 후보 수를 줄이면 성능이 바로 떨어질 수 있다는 뜻이다.

예를 들어 rerank pool을 fused top50으로 자르면 다음 케이스들은 위험해진다.

idxfusedrerank
8601
15541
31791
38691
47601
51591
53711
73604
75722

지금은 latency 최적화보다 pool 보존 정책이 먼저다.

rerank_pool =
  dense_topN
  + sparse_topN
  + exact_entity_match_topN
  + alias_match_topN
  + source/title match candidates

최종 순위를 강제로 올리자는 뜻은 아니다. 적어도 reranker가 판단할 후보군에서 gold-like 문서가 사라지지 않게 해야 한다는 뜻이다.

3. reranker가 항상 정답은 아니다

반대로 reranker가 gold를 밀어낸 케이스도 있다.

idxdensesparsefusedrerank
91113
171114
681112
711115

이 케이스들은 first-stage retrieval이 이미 gold를 1위로 잡았다. 그런데 reranker가 2~5위로 밀었다. 이건 sparse나 dense 문제가 아니다.

가능성은 세 가지다.

  • reranker가 문서의 관련도를 잘못 판단했다.
  • qrels의 gold 문서 외에도 더 좋은 문서가 있었다.
  • chunk scope나 parent document 기준이 평가 기준과 어긋났다.

따라서 teacher distillation을 할 때도 현재 reranker score를 그대로 gold처럼 쓰면 위험하다. reranker가 이미 틀리는 케이스가 있다면, 그 점수를 student retriever가 그대로 학습하게 된다.

teacher score는 “정답”이 아니라 supervision 후보로 봐야 한다.

teacher label =
  gold qrels
  + reranker score
  + entity/exact match rule
  + source/title validation
  + human spot check

Qdrant hybrid에서 조심할 점

현재 구조가 Qdrant native hybrid라면 한 가지를 더 조심해야 한다. Qdrant의 Query API는 dense와 sparse prefetch 결과를 받아 RRF나 DBSF로 fusion할 수 있다(Qdrant Hybrid Queries). 최신 문서 기준으로 weighted RRF도 지원한다.

하지만 이것이 Elasticsearch식 field boost와 같지는 않다.

title^5
equipment^10
component^8
body^1
alias_terms^2

이런 식의 lexical field scoring을 그대로 기대하면 안 된다. 도메인 검색에서 title, source_pdf, component, action, alarm 같은 필드가 강한 의미를 가진다면 application layer에서 custom fusion을 하거나, named vector와 payload 기반 후보 보존 전략을 별도로 설계해야 한다.

내가 지금 원하는 것은 “sparse 점수를 조금 더 주자” 정도가 아니다. 특정 도메인 entity가 강하게 일치하는 문서가 rerank pool에서 빠지지 않게 만드는 것이다.

내가 다음에 할 일

이 trace를 보고 우선순위를 다시 정리했다.

1. 빠진 5개를 먼저 본다

Top5 Hits 표에 없는 4, 21, 40, 42, 66이 진짜 miss다. 이 다섯 개에 대해 아래 항목을 모두 확인해야 한다.

query
gold_doc_id
gold_chunk_id
dense_rank
sparse_rank
fused_rank
rerank_rank
gold_in_dense_topN
gold_in_sparse_topN
gold_in_fused_topN
gold_in_rerank_pool
top false positives
matched aliases
missing aliases
diagnosis

특히 #40 Baratron처럼 도메인 alias가 강하게 의심되는 케이스는 sparse/search_text 문제인지, reranker 문제인지, qrels 문제인지 따로 봐야 한다.

2. 실패 유형을 자동 분류한다

처음부터 복잡한 진단 모델은 필요 없다. 규칙 기반으로도 충분히 시작할 수 있다.

def diagnose(row):
    if not row.gold_in_dense_top50 and not row.gold_in_sparse_top50:
        return "index_or_alias_miss"

    if row.gold_in_sparse_top50 and not row.gold_in_fused_top50:
        return "fusion_drop_sparse_hit"

    if row.gold_in_fused_top50 and not row.gold_in_final_top10:
        return "reranker_demotion"

    if row.gold_in_sparse_top50 and not row.gold_in_rerank_pool:
        return "pool_truncation"

    if row.gold_in_dense_top50 and not row.gold_in_sparse_top50:
        return "sparse_alias_miss"

    if row.gold_in_sparse_top50 and not row.gold_in_dense_top50:
        return "dense_semantic_miss"

    return "needs_review"

중요한 건 완벽한 분류가 아니다. 실패를 한 덩어리로 보지 않는 것이다.

3. alias와 search_text를 먼저 보강한다

SPLADE 학습 전에 바로 적용할 수 있는 개선이 있다.

  • source_pdf, title, section heading을 search_text에 명시적으로 포함한다.
  • 장비명, 부품명, action, alarm alias를 승인 기반으로 관리한다.
  • query preprocessing에서 약어를 확장한다.
  • 문서 search_text에도 같은 alias를 넣어 query-document 양쪽 mismatch를 줄인다.

document-only expansion만으로는 부족할 수 있다. 지금 문제는 query-side alias도 크기 때문이다. 사용자가 REP라고 묻는데 문서에는 Replacement만 있고, query sparse vector에는 REP만 남아 있으면 매칭이 약할 수 있다.

4. rerank pool 보존 규칙을 둔다

현재 trace에서는 reranker가 gold를 많이 살린다. 그러면 gold-like candidate가 reranker에 도달하는 것이 중요하다.

pool = union(
  dense_top_80,
  sparse_top_120,
  fused_top_80,
  exact_component_match_top_30,
  source_title_match_top_30,
)

숫자는 평가셋으로 조정해야 한다. 핵심은 “fused top50만 rerank한다”처럼 단순히 자르면 안 된다는 점이다.

5. 그 다음 qrels/triples를 만든다

trace와 alias 보강 후에도 남는 실패 케이스가 진짜 학습 데이터다.

{
  "query": "PM Flow Switch REP procedure",
  "positive_doc_id": "global sop_supra xp_all_pm_flow switch.pdf",
  "hard_negative_doc_ids": [
    "global sop_supra xp_all_ll_flow switch.pdf",
    "global sop_supra xp_all_efem_pressure switch.pdf",
    "global sop_supra xp_all_pm_pressure gauge.pdf"
  ]
}

hard negative는 임의로 고르면 안 된다. 실제 시스템이 상위에 올렸지만 정답이 아니었던 문서가 가장 좋은 hard negative다.

SPLADE는 언제 해야 하나

SPLADE 방향이 틀렸다는 뜻은 아니다. 오히려 이 trace는 SPLADE가 필요한 이유도 보여준다. sparse rank가 너무 낮은 케이스가 많기 때문이다.

다만 순서가 중요하다.

SPLADE가 해결할 수 있는 문제는 주로 이런 것들이다.

  • lexical mismatch
  • 도메인 용어 확장
  • sparse retriever의 recall 부족
  • query-document 표현 차이

반대로 SPLADE가 해결하면 안 되는 문제도 있다.

  • reranker가 gold를 밀어낸 문제
  • fusion에서 sparse exact match를 버린 문제
  • rerank pool이 너무 좁아서 gold가 사라진 문제
  • qrels가 틀린 문제
  • chunk scope가 평가 기준과 어긋난 문제
  • metadata 필터가 잘못된 문제

이 구분 없이 바로 fine-tuning을 하면, 모델 학습이 아니라 실패 원인을 비싸게 덮는 작업이 된다.

내 현재 로드맵은 이 순서다.

1. retrieval stage trace 완성
2. miss 케이스 자동 분류
3. alias/search_text/query expansion 보강
4. rerank pool 보존 및 custom fusion 검토
5. qrels와 hard negative 구축
6. teacher score 생성
7. SPLADE 또는 Inference-Free SPLADE fine-tuning
8. hard negative mining 반복

정리

처음에는 retrieval 성능이 막히면 더 좋은 모델을 찾고 싶어진다. 나도 그랬다. hard coding으로 alias를 추가하고 search_text를 늘리는 방식이 한계에 닿자, SPLADE와 distillation을 조사하기 시작했다.

그런데 stage trace를 만들고 나니 결론이 조금 달라졌다.

지금 당장 필요한 것은 SPLADE 학습이 아니라, 검색 실패가 어느 단계에서 발생하는지 분리하는 일이었다. sparse는 약했고, reranker는 강하게 회복하고 있었고, 일부 케이스에서는 reranker가 오히려 gold를 밀어내고 있었다. 이 상태에서 최종 gold_rank만 보고 모델을 바꾸면 원인을 잘못 짚을 가능성이 크다.

이번 글의 결론은 단순하다.

retrieval 성능이 한계에 부딪쳤을 때, 첫 번째 작업은 모델 교체가 아니다. gold 문서가 dense, sparse, fusion, rerank 중 어디서 사라지는지 기록하는 것이다.

SPLADE는 그 다음이다. trace가 쌓이고, alias/search_text로 회복 가능한 부분을 먹고, 그래도 남는 실패 케이스가 qrels와 hard negative로 정리됐을 때 SPLADE 학습은 비로소 의미가 생긴다.


참고자료