idea_world_labDEV JOURNAL
2026년 6월 28일 일요일

Retriever 검색 대안 분해 문서

작성일: 2026년 6월 28일

이 디렉토리는 Retriever 검색 대안 ChatGPT 원문 메모를 보기 쉽게 다시 나눈 문서 묶음이다.

원문 메모는 그대로 보존한다. 이 README에는 공통 맥락, 현재 방식, 모델 후보, 선택 가이드를 합쳤다. 하위 문서는 대안별 문서만 둔다.

종합 비교표

이 표들은 아직 최종 선택지가 아니라, 6월 28일에 비교해야 할 검색 대안을 한 화면에서 판단하기 위한 관찰표다. 기준은 “코드 chunk를 그대로 Retriever에 넣었을 때 어떤 JSONL 후보가 올라오고, 그 후보가 Qwen 검증 단계에서 직접 근거로 인정될 수 있는가”다.

한눈에 보는 결론

대안 위치 기대 역할 바로 쓸 수 있는 정도 핵심 장점 핵심 위험 현재 판단
A. PostgreSQL full-text 유지 현재 /api/retrieve 기준선 지금 구현된 검색이 어느 정도까지 되는지 확인 높음 이미 붙어 있고 로그를 바로 볼 수 있음 긴 코드 chunk가 query가 되면 관련 문서도 누락될 수 있음 기준선으로 남기되, 최종 검색 방식으로 단정하지 않는다
B. BM25 only 1차 검색 후보 코드/API 문자열이 직접 들어간 문서를 찾기 중간 KinematicBody2D, move_and_slide, AnimatedSprite2D 같은 직접 문자열에 강함 2D/3D처럼 넓은 단어가 섞이면 false positive가 남음 첫 PoC로 가장 관찰하기 좋다
C. embedding only 의미 기반 recall 확장 문자열이 정확히 같지 않아도 관련 설명 문서를 찾기 중간 문장 의미, 튜토리얼 설명, 유사한 사용 맥락을 넓게 가져옴 버전/API 정확도 판정에는 단독 사용 위험이 큼 단독 사용보다 보조 recall로 둔다
D. BM25 + embedding 현실적인 중간안 문자열 근거와 의미 근거를 함께 모으기 중간 BM25의 정확성과 embedding의 recall을 같이 씀 점수 결합, 중복 제거, 상위 후보 기준이 필요함 실사용 최소안에 가깝다
E. Qwen query profile 검색 보조 실험 코드 chunk에서 검색 의도를 JSON으로 만들기 낮음 사람이 보는 query profile은 읽기 쉬움 검색 전에 LLM이 없는 API/의도를 만들어낼 수 있음 1차 검색 핵심으로 쓰기보다 실험/보조로 둔다
F. BM25 + embedding + reranker + validator 최종 후보 구조 후보 생성, 재정렬, 직접 근거 검증까지 연결 낮음 품질과 관찰성이 가장 좋음 비용, latency, 단계 수가 많음 최종 목표 후보로 둔다

PoC 시뮬레이션 결과 비교

대안 입력 chunk 검색 후보가 나오는 방식 관련 JSONL이 올라올 가능성 관련 없는 JSONL이 섞일 가능성 Qwen 검증 전 기대 상태 Qwen 검증 후 기대 상태
A chunkText 전체 plainto_tsquery('simple', chunkText)로 긴 query 생성 낮거나 불안정 낮을 수도 있지만, 0건이면 관찰할 후보가 없음 후보가 없거나 일부만 반환 직접근거 검증까지 가기 전에 누락 가능
B chunkText 전체 토큰별 BM25 점수로 문서 순위화 높음 중간 Input.is_action_pressed, AnimatedSprite2D, clamp 문서가 올라올 수 있음 직접 문자열이 없으면 탈락
C chunkText 전체 code/document embedding 유사도 중간 높음 “movement”, “2D”, “player” 설명 문서가 넓게 올라옴 JSONL 안 직접 문자열이 없으면 탈락
D chunkText 전체 BM25 후보와 embedding 후보를 합침 높음 중간 문자열 후보와 의미 후보가 함께 보임 직접근거 있는 후보만 남김
E chunkText 전체를 Qwen이 해석 Qwen이 만든 검색 profile로 검색 profile이 맞으면 중간 높음 profile에 없는 API가 섞일 수 있음 profile 자체가 실제 코드와 맞는지도 검증 필요
F chunkText 전체 BM25 + embedding 후보를 reranker가 재정렬 높음 낮음 후보가 많아도 상위가 정리됨 Qwen이 “예/아니오”로 직접근거만 남김

데모 판정에서 드러난 차이

관찰 상황 처음 단순 프롬프트 개선한 직접근거 프롬프트 검색 구조에 주는 의미
Godot 3 코드 + 관련 JSONL “예”가 나올 수 있음 “예”가 나와야 함 관련 근거가 실제 문자열/API와 맞으면 통과
Godot 3 코드 + 무관 JSONL 넓은 Godot 단어 때문에 “예”가 나올 수 있음 “아니오”가 나와야 함 검색 후보가 올라와도 직접근거 검증 없이는 위험
KinematicBody2D 코드 + 3D Spatial mapping Godot migration이라는 넓은 의미로 관련 있다고 볼 수 있음 “아니오”가 맞음 embedding only나 느슨한 LLM 판단은 false positive를 만들 수 있음
yield(...) 코드 + await prototype 직접 패턴이 있으면 “예” “예” label_prototypes는 통째 사용 패턴 변경을 잡는 근거가 될 수 있음
AnimatedSprite2D.play() 코드 + 일반 animation docs 의미상 관련 있어 보일 수 있음 JSONL에 직접 문자열/패턴이 없으면 “아니오” docs_chunks도 최종 판단에는 직접근거 확인이 필요

후보 반환 양상 비교

대안 상위에 잘 올라오는 후보 놓치기 쉬운 후보 잘못 섞이기 쉬운 후보 눈으로 확인할 포인트
A 한 row 안에 query 토큰을 많이 가진 문서 관련 토큰이 여러 문서에 나뉜 경우 query가 너무 엄격하면 거의 없음 왜 0건인지, 어떤 토큰 때문에 매칭이 깨졌는지
B API 이름, 함수명, 노드명이 직접 들어간 row 표현이 바뀐 설명형 문서 같은 단어를 가진 다른 시스템 문서 match_terms와 실제 source chunk 문자열 일치 여부
C 의미가 비슷한 튜토리얼/설명 문서 짧은 exact API mapping Godot 버전이나 2D/3D가 다른 문서 cosine 점수보다 직접 문자열 근거가 있는지
D exact 후보 + semantic 후보 점수 결합에서 밀린 후보 두 검색이 모두 약하게 맞은 후보 후보가 BM25에서 왔는지 embedding에서 왔는지
E Qwen이 뽑은 의도와 맞는 후보 Qwen이 profile에 넣지 않은 실제 코드 단서 Qwen이 상상한 API/의도 관련 후보 profile의 각 필드가 실제 chunk 안에 존재하는지
F reranker가 chunk와 맞다고 본 후보 reranker 모델이 모르는 Godot 특수 케이스 reranker 점수는 높지만 JSONL 직접근거가 없는 후보 rerank score와 validator 판정이 갈리는 지점

False Positive / False Negative 비교

대안 false positive 예 false negative 예 줄이는 방법
A 드물지만 query가 넓게 풀리면 일반 movement 문서가 섞임 관련 문서가 여러 row에 나뉘면 0건 query 생성 방식을 바꾸거나 BM25 후보를 추가
B movement, player, 2d 때문에 다른 movement 문서가 섞임 API 이름이 문서에는 설명형으로만 있으면 누락 stopword/약한 토큰 분리, 직접 API 토큰 가중
C 의미가 비슷한 3D/다른 버전 문서가 섞임 짧은 API rename row를 못 잡음 BM25와 병렬로 쓰고 validator를 붙임
D BM25와 embedding 양쪽에서 약하게 맞은 후보가 섞임 fusion 기준이 잘못되면 한쪽 후보가 밀림 union source flag와 direct evidence log를 남김
E Qwen이 없는 migration_intent를 만들어 후보를 오염시킴 Qwen이 실제 코드 단서를 profile에 누락 profile을 검색 전 신뢰하지 않고 보조로만 사용
F reranker가 말이 되는 설명을 과대평가 후보 생성 단계에서 아예 못 가져온 문서 후보 수를 충분히 확보하고 Qwen 검증을 마지막에 둠

테이블별 적합도

대안 docs_chunks api_mapping label_prototypes 비고
A 긴 설명 문서에서 불안정 exact mapping row가 짧으면 놓칠 수 있음 pattern row가 짧으면 놓칠 수 있음 현재 방식의 한계를 확인하는 기준선
B API/heading/token 검색에 좋음 source/target API exact 검색에 좋음 source/target pattern 검색에 좋음 세 테이블 모두 첫 검색 후보로 관찰하기 좋음
C 설명형 문서 recall에 좋음 API rename처럼 짧은 row에는 약함 사용 패턴 의미가 비슷하면 보조 가능 docs_chunks 보조에 가장 자연스러움
D 설명/문자열 후보를 모두 확보 exact mapping과 semantic 후보를 함께 확보 통째 사용 패턴 후보를 넓게 확보 세 테이블 공통 구조로 가장 무난
E query profile이 잘 나오면 설명 문서 검색 가능 hallucinated API가 들어가면 위험 prototype 의도를 만들 수 있지만 오염 가능 검색 핵심이 아니라 관찰/보조 실험에 가깝다
F 최종 설명 근거 정제에 좋음 migration 근거 정제에 좋음 사용 방식 변경 근거 정제에 좋음 세 테이블을 같은 파이프라인으로 다루기 좋음

로그로 반드시 봐야 하는 것

대안 검색 전 로그 검색 중 로그 검색 후 로그 실패 원인 확인 포인트
A raw chunkText, 생성된 tsquery search_tsv @@ query hit 수, ts_rank_cd 반환 row, score query가 너무 길어졌는지, 어떤 토큰이 row에 없는지
B raw chunkText, token list term frequency, matched terms, BM25 score table별 top-k 강한 토큰과 약한 토큰이 구분되는지
C raw chunkText, embedding model vector distance/cosine score semantic top-k 실제 문자열 없이 의미만 맞는 후보인지
D BM25 top-k, embedding top-k union, duplicate merge, score fusion source flags가 붙은 후보 목록 한쪽 검색만 가져온 후보가 왜 살아남았는지
E Qwen에 보낸 raw chunk Qwen query profile JSON profile 기반 검색 결과 profile 필드가 실제 source에 존재하는지
F BM25/embedding 후보 전체 rerank score, rerank order Qwen 예/아니오, reject reason reranker 통과 후보가 직접근거 검증에서 왜 탈락했는지

구현 복잡도와 운영 비용

대안 구현량 DB 변경 외부 모델 비용 latency 디버깅 난이도 실패했을 때 고치기 쉬운 정도
A 낮음 거의 없음 없음 낮음 낮음 낮음. 다만 성능 한계가 구조적일 수 있음
B 중간 BM25 검색 엔진/인덱스 선택 필요 없음 또는 낮음 낮음 낮음 높음. 토큰/가중치 조정이 가능
C 중간 embedding 컬럼/인덱스 필요 있음 중간 중간 중간. 모델/차원/threshold 조정 필요
D 중간~높음 BM25 + vector 둘 다 필요 있음 중간 중간 높음. 어느 검색 경로가 실패했는지 분리 가능
E 중간 profile 저장은 선택 있음 높음 높음 낮음. LLM profile이 왜 틀렸는지 재현이 어려울 수 있음
F 높음 BM25 + vector + rerank log 필요 있음 높음 높음 높음. 단계별 로그가 있으면 고칠 지점이 보임

Qwen을 어디에 둘지 비교

위치 장점 위험 현재 더 맞는 사용 방식
검색 전 query 생성 하드코딩 없이 검색 의도를 만들 수 있음 없는 단서를 만들어 검색을 오염시킬 수 있음 실험용으로만 둔다
검색 후보 후 직접근거 검증 검색 결과가 실제 코드와 맞는지 걸러낼 수 있음 호출 비용이 듦 가장 중요하게 본다
최종 답변 생성 코드 설명/마이그레이션 응답을 만들 수 있음 근거가 틀리면 답변도 틀림 검증된 JSONL 후보 이후에 둔다
전체 판단 통합 사람이 읽기 좋은 결론을 만들 수 있음 중간 실패를 숨길 수 있음 로그가 충분히 쌓인 뒤 검토한다

선택 우선순위

우선순위 선택지 이유 이 단계에서 확인할 질문
1 A를 기준선으로 측정 현재 구현이 어디서 깨지는지 알아야 함 지금 방식이 실제로 몇 건을 반환하는가
2 B를 별도 PoC로 붙임 코드/API 문자열 검색이 가장 설명 가능함 KinematicBody2D, yield, move_and_slide가 올바른 row를 찾는가
3 C를 보조 recall로 비교 docs_chunks 설명형 후보를 넓힐 수 있음 의미 후보가 직접근거 검증에서 얼마나 탈락하는가
4 D로 병렬 후보 생성 현실적인 최소 구조 BM25와 embedding 후보를 합쳤을 때 품질이 좋아지는가
5 F로 reranker/validator 추가 최종 품질 확인 Qwen 직접근거 판정이 무관 JSONL을 안정적으로 버리는가
보류 E 검색 전 LLM은 오염 위험이 큼 profile이 실제 코드 문자열만 기반으로 만들어지는가

PoC 체크리스트

체크 항목 봐야 하는 화면/로그 성공 기준
같은 chunkText를 여러 대안에 넣기 웹 디버거의 chunk 입력 영역 입력이 대안별로 달라지지 않는다
테이블별 검색 버튼 docs_chunks, api_mapping, label_prototypes 버튼 같은 chunk로 테이블별 후보 차이가 보인다
raw 후보 확인 반환된 JSONL payload source chunk 안 실제 문자열과 맞는 필드가 보인다
Qwen 검증 확인 prompt + chunk + jsonl 디버그 영역 관련 후보는 “예”, 무관 후보는 “아니오”가 나온다
reject reason 확인 validator 결과 로그 Godot, migration, 2D 같은 넓은 단어만 맞은 후보가 탈락한다
false positive 수집 탈락 후보 목록 나중에 query/token/validator 규칙 개선에 쓸 수 있다
대안별 비교 저장 관찰일지 또는 결과 JSON 같은 입력에 대한 A-F 차이가 남는다

대안 문서

현재 전제

현재 기준은 다음과 같다.

1. zip 안에는 markdown 원본, 스키마, 디버거, 설계 문서가 있다.
2. 로컬에는 이미 markdown -> JSONL -> DB 변환본이 들어가 있다.
3. 지금 볼 수 있는 것은 변환 전 markdown과 DB 스키마/검색 흐름이다.
4. PoC는 zip 안 markdown을 JSONL chunk처럼 쪼개서, 로컬 DB에 들어갔을 법한 검색 대상을 시뮬레이션해야 한다.
5. 필요한 것은 코드 뼈대가 아니라, chunk가 들어갔을 때 각 검색 방식이 어떻게 점수를 만들고 왜 성공/실패하는지 추적하는 것이다.

JSONL/DB 기준

문서 기준 산출물은 다음이다.

work/godot_rag/jsonl/docs_chunks.jsonl
work/godot_rag/jsonl/api_mapping.jsonl
work/godot_rag/jsonl/label_prototypes.jsonl
work/godot_rag/jsonl/ingest_report.jsonl

DB는 JSONL 한 줄을 통째로 payload jsonb에 넣는 구조가 아니라, JSONL 필드가 테이블 컬럼으로 펼쳐지는 구조로 이해한다.

docs_chunks

chunk_id
doc_version
source_url
source_file
source_sha256
doc_type
symbol
section_path
heading
content
code_blocks
language_tags
godot_version_tags
api_symbols
token_count
metadata
embedding
search_tsv

api_mapping

mapping_id
source_api
target_api
change_type
godot_from
godot_to
confidence
evidence_chunk_ids
match_terms
notes
negative_patterns

label_prototypes

prototype_id
label
task_type
input_pattern
expected_finding
recommended_action
evidence_mapping_ids
evidence_chunk_ids
severity
validator_rules
embedding
search_tsv

기준 chunk

대안 비교에서 기준으로 삼은 chunk는 다음 Godot 코드다.

func _process(delta):
	var velocity = Vector2.ZERO
	if Input.is_action_pressed(&"move_right"):
		velocity.x += 1
	if Input.is_action_pressed(&"move_left"):
		velocity.x -= 1

	if velocity.length() > 0:
		velocity = velocity.normalized() * speed
		$AnimatedSprite2D.play()
	else:
		$AnimatedSprite2D.stop()

	position += velocity * delta
	position = position.clamp(Vector2.ZERO, screen_size)

강한 토큰:

animatedsprite2d
is_action_pressed
move_left
move_right
normalized
clamp
screen_size
vector2
zero

약한 토큰:

func
var
if
else
velocity
position
delta
x
0
1

검색 입력은 계속 chunkText 그대로 유지한다. 검색 전에 Godot API signal을 필수로 추출해서 별도 query로 바꾸는 방식은 기본 설계의 핵심으로 두지 않는다. 필요하다면 나중에 보조 점수 정도로만 검토한다.

현재 /api/retrieve 방식

현재 프로젝트의 /api/retrieve는 BM25가 아니다.

흐름은 다음에 가깝다.

chunkText
  -> plainto_tsquery('simple', chunkText)
  -> search_tsv @@ query
  -> ts_rank_cd(...)

PostgreSQL의 ts_rank_cd는 full-text ranking 함수다. BM25처럼 term frequency, inverse document frequency, field length normalization을 조합하는 검색 엔진식 BM25 자체와는 다르게 봐야 한다.

plainto_tsquery(raw_chunk)는 입력 chunk에서 토큰을 뽑아 query를 만든다. 기준 chunk는 다음처럼 긴 query가 될 수 있다.

func & process & delta & var & velocity & vector2 & zero
& input & is_action_pressed & move_right & move_left
& length & normalized & speed & animatedsprite2d
& play & stop & position & clamp & screen_size

이 방식은 한 row 안에 너무 많은 토큰을 요구할 수 있다.

공식문서 chunk가 다음처럼 나뉘어 있으면 문제가 생긴다.

chunk A:
  Input.is_action_pressed
  velocity.normalized
  AnimatedSprite2D.play/stop

chunk B:
  AnimatedSprite2D 설명
  position.clamp
  screen_size

둘 다 관련 있지만, 하나의 row에 모든 토큰이 동시에 들어있지는 않을 수 있다.

원문 메모에서는 strict raw AND 성격의 검색이 다음처럼 실패할 수 있다고 본다.

strict raw AND hits: 0

현재 방식은 PoC의 기준선으로는 볼 수 있지만, 최종 검색 전략으로 유지하기에는 약하다. 다음 단계에서는 현재 방식과 BM25를 분리해서 비교해야 한다.

모델 후보

voyage-code-3

현재 프로젝트에 가장 잘 맞는 embedding 후보로 언급됐다.

이유:

query가 자연어 질문이 아니라 GDScript code chunk다.
검색 대상 JSONL에도 code_blocks, API 이름, 문서 설명이 섞여 있다.

장점:

  • code retrieval에 강하다.
  • 긴 chunk 처리에 유리하다.
  • code -> docs 검색에 적합하다.

단점:

  • 외부 API 의존이 있다.
  • 비용이 든다.
  • Godot 특화 모델은 아니다.

메모 기준 추천:

voyage-code-3 1024 float

OpenAI text-embedding-3-large

범용 고품질 embedding 후보.

장점:

  • 범용 semantic retrieval에 강하다.
  • 문서 설명 검색에 안정적이다.
  • OpenAI ecosystem과 붙이기 쉽다.

단점:

  • code retrieval 전용은 아니다.
  • 기본 차원이 커서 저장/인덱스 비용이 커질 수 있다.
  • GDScript chunk -> JSONL 검색에는 voyage-code-3보다 덜 직접적일 수 있다.

메모 기준 추천도:

2순위

Gemini Embedding

범용/다국어 semantic retrieval 후보.

장점:

  • 문서 설명형 retrieval에 좋다.
  • 차원 축소 선택지가 있다.
  • 다국어 문서 검색에 강점이 있을 수 있다.

단점:

  • code retrieval 전용 선택지는 아니다.
  • API 코드 정확성보다 의미 유사성에 가깝다.
  • Godot migration exact 판단에는 단독 사용 위험이 있다.

Jina embeddings v4

복잡한 문서, multimodal, visually rich document retrieval에 강점이 있는 후보.

장점:

  • 문서 검색 폭이 넓다.
  • multimodal/복합 문서에 강하다.
  • code adapter 계열도 검토 가능하다.

단점:

  • 현재 프로젝트는 markdown/code 중심이다.
  • 이미지/표 retrieval이 핵심이 아니다.
  • 지금 단계에서는 과한 선택일 수 있다.

메모 기준 추천도:

지금 당장은 우선순위 낮음

rerank-2.5

reranker 후보로 언급됐다.

역할:

BM25와 embedding이 가져온 후보를 raw chunk와 함께 다시 비교한다.
관련도 점수로 후보를 재정렬한다.

이 프로젝트에서 기대하는 효과:

  • 2D movement와 3D movement false positive를 낮춘다.
  • AnimatedSprite2D, Vector2, screen_size, position.clamp 같은 직접 문맥을 더 잘 반영한다.
  • BM25와 embedding이 각각 가져온 후보를 최종 후보로 정리한다.

주의:

  • reranker는 validator가 아니다.
  • 직접근거 검증은 Qwen direct-evidence validator가 다시 해야 한다.

선택지 요약

대안 흐름 장점 위험
A PostgreSQL full-text 유지 이미 구현됨 긴 chunk에서 0건 가능, BM25 아님
B BM25 only 투명함, 문자열/API에 강함 3D movement 같은 false positive
C embedding only 의미 검색 가능 API/버전 정확도 약함
D BM25 + embedding 현실적인 균형 점수 결합 튜닝 필요
E Qwen query profile 하드코딩 감소, 의도 요약 가능 hallucination, 비용, 검색 전 오염
F BM25 + embedding + reranker + validator 품질 최우선 비용, latency, 단계 많음

현재 임시 판단

원문 메모의 결론은 다음에 가깝다.

입력은 chunkText 하나만 유지한다.

1차 검색은 BM25로 한다.
  이유: 코드/API 문자열 검색에 투명하고 강하다.

2차 검색은 voyage-code-3 embedding으로 한다.
  이유: query가 코드 chunk라서 code retrieval 모델이 맞다.

3차 정렬은 rerank-2.5로 한다.
  이유: BM25와 embedding이 가져온 비슷한 후보 중 진짜 chunk와 맞는 JSONL을 위로 올리기 위해서다.

마지막 검증은 Qwen direct-evidence validator로 한다.
  이유: JSONL 안에 chunk와 직접 맞는 문자열/패턴 근거가 없으면 버려야 하기 때문이다.

한 줄 요약:

raw chunk 그대로 입력
  -> BM25 + code embedding 병렬 후보 생성
  -> reranker 재정렬
  -> Qwen 직접근거 검증

다음에 실제로 비교할 것

  • 현재 PostgreSQL full-text 방식이 기준 chunk에서 몇 건을 반환하는지
  • BM25가 first_2d_game을 실제로 상위에 올리는지
  • BM25 only에서 3D movement false positive가 얼마나 섞이는지
  • embedding only가 어떤 설명형 문서를 추가로 가져오는지
  • BM25 + embedding union에서 중복과 점수를 어떻게 합칠지
  • reranker가 2D/3D 후보를 제대로 재정렬하는지
  • Qwen direct-evidence validator가 관련 없는 JSONL을 실제로 아니오로 버리는지