결론부터
맞습니다. 내가 의도를 틀리게 잡았습니다.
정확히 다시 잡으면 이겁니다.
1. zip 안에는 markdown 원본, 스키마, 디버거, 설계 문서가 있다.
2. 네 로컬에는 이미 markdown -> JSONL -> DB 변환본이 들어가 있다.
3. 내가 볼 수 있는 건 “변환 전 markdown과 DB 스키마/검색 흐름”이다.
4. 그래서 PoC는 zip 안 markdown을 JSONL chunk처럼 쪼개서, 네 로컬 DB에 들어갔을 법한 검색 대상을 시뮬레이션해야 한다.
5. 네가 원하는 건 코드 뼈대가 아니라, chunk가 들어갔을 때 각 검색 방식이 어떻게 점수를 만들고 왜 성공/실패하는지 흐름을 추적하는 것이다.그리고 중요한 수정 하나.
현재 프로젝트의 /api/retrieve는 BM25가 아닙니다.
현재 코드는:
chunkText
-> plainto_tsquery('simple', chunkText)
-> search_tsv @@ query
-> ts_rank_cd(...)입니다. PostgreSQL의 ts_rank_cd는 full-text ranking이고, BM25 자체가 아닙니다. PostgreSQL 문서도 ts_rank/ts_rank_cd가 문서 길이 normalization 옵션을 받는 ranking 함수라고 설명합니다. plainto_tsquery는 입력 문장을 토큰화해서 punctuation을 무시하고 non-stopword들을 query로 만듭니다. (PostgreSQL)
결론부터
네 조건을 만족하는 최선은 이겁니다.
입력:
raw chunkText 그대로
Retriever 내부:
1. raw chunk 그대로 BM25 후보 검색
2. raw chunk 그대로 code embedding 후보 검색
3. 후보 union
4. reranker로 재정렬
5. Qwen 또는 validator가 JSONL 직접근거 검증Godot API signal 추출을 필수로 두지 않는 버전입니다.
즉 내가 이전에 말한 “Godot API signal extractor”는 필수로 두면 안 됩니다. 네 말대로 그건 하드코딩 냄새가 납니다. 쓸 수는 있지만, 기본 설계의 핵심으로 두면 유지보수가 나빠집니다.
최종 추천은:
1차 후보: BM25
2차 후보: code embedding
최종 정렬: reranker
검증: Qwen direct-evidence validator프로젝트 이해 검증
zip에서 확인한 실제 구조는 이렇습니다.
JSONL 설계
문서에는 산출물이 이렇게 정의되어 있습니다.
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.jsonlDB 스키마는 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즉 네 로컬 DB에는 이런 식으로 들어가 있을 겁니다.
{"chunk_id":"...","source_file":"outputs/godot_docs_full/pages/getting_started__first_2d_game__03.coding_the_player__161d377b.md","doc_type":"tutorial","heading":"Coding the player","content":"...","code_blocks":["func _process(delta): ..."],"api_symbols":["Input.is_action_pressed","Vector2.ZERO","AnimatedSprite2D","position.clamp"]}네 chunk 입력
입력은 무조건 이거 그대로라고 가정합니다.
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)이 chunk를 code tokenizer로 보면 unique token은 이렇게 나옵니다.
_process
animatedsprite2d
clamp
delta
else
func
if
input
is_action_pressed
length
move_left
move_right
normalized
play
position
screen_size
speed
stop
var
vector2
velocity
x
zero
0
1총 43개 토큰, unique 25개입니다.
여기서 중요한 토큰은 이겁니다.
강한 토큰:
animatedsprite2d
is_action_pressed
move_left
move_right
normalized
clamp
screen_size
vector2
zero
약한 토큰:
func
var
if
else
velocity
position
delta
x
0
1BM25는 강한 토큰에 높은 점수를 줍니다. 왜냐하면 문서 전체에서 드물게 나오는 단어일수록 IDF가 커지기 때문입니다.
BM25가 어떻게 동작해서 결과가 나오는가
BM25는 대충 “query 단어가 document에 얼마나 잘 박혀 있냐”를 계산합니다.
공식적으로는 세 가지가 핵심입니다.
TF: 이 document 안에서 단어가 몇 번 나왔는가
IDF: 전체 corpus에서 이 단어가 얼마나 드문가
Length normalization: 긴 document가 무조건 유리해지지 않게 보정Elasticsearch도 BM25를 기본 relevance 알고리즘으로 쓰며, term frequency, inverse document frequency, field-length normalization을 핵심 요소로 설명합니다. (Elastic)
식은 이런 느낌입니다.
score(query, doc)
= Σ over query terms [
IDF(term)
*
TF_boost(term frequency in doc, doc length)
]즉 animatedsprite2d처럼 드문 단어가 document에 여러 번 있으면 점수가 크게 오릅니다.
실제처럼 시뮬레이션한 결과
zip 안의 실제 Godot markdown 원본:
outputs/godot_docs_full/pages/이걸 JSONL docs_chunks처럼 1800자 내외 chunk로 쪼개고, 네 raw chunk를 그대로 query로 넣어 BM25를 돌렸습니다.
후보 1위
score: 111.799
source_file:
outputs/godot_docs_full/pages/getting_started__first_2d_game__03.coding_the_player__161d377b.md
chunk 내용:
Input.is_action_pressed("move_right")
Input.is_action_pressed("move_left")
velocity.length()
velocity.normalized() * speed
$AnimatedSprite2D.play()
$AnimatedSprite2D.stop()왜 1위냐면, 이 토큰들이 직접 맞았습니다.
animatedsprite2d
is_action_pressed
move_left
move_right
normalized
play
stop
input
speed
length
velocity
vector2
zero점수를 크게 만든 상위 기여 토큰은 이렇습니다.
animatedsprite2d df=18 tf=7 contribution=11.975
is_action_pressed df=27 tf=2 contribution=8.609
move_left df=13 tf=1 contribution=7.479
velocity df=180 tf=18 contribution=7.467
move_right df=15 tf=1 contribution=7.299
normalized df=67 tf=2 contribution=7.069
stop df=133 tf=2 contribution=5.900
play df=144 tf=2 contribution=5.764
input df=323 tf=7 contribution=5.650
speed df=219 tf=3 contribution=5.637여기서 df는 전체 chunk 4165개 중 해당 단어가 들어간 chunk 수입니다.
즉:
animatedsprite2d는 전체 4165개 chunk 중 18개에만 등장
move_left는 13개에만 등장
move_right는 15개에만 등장
is_action_pressed는 27개에만 등장그래서 이 단어들이 있는 문서가 강하게 올라옵니다.
이게 BM25가 “어떻게” 1위를 만든 과정입니다.
후보 2위
score: 80.467
source_file:
outputs/godot_docs_full/pages/getting_started__first_2d_game__03.coding_the_player__161d377b.md
chunk 내용:
AnimatedSprite2D 설명
$AnimatedSprite2D.play()
get_node("AnimatedSprite2D").play()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)매칭된 토큰:
animatedsprite2d
clamp
screen_size
play
delta
_process
velocity
position
stop
vector2
zero이 chunk가 2위인 이유는 입력 처리 부분은 없지만, clamp, screen_size, AnimatedSprite2D가 강하게 맞기 때문입니다.
특히:
screen_size df=3
clamp df=32
animatedsprite2d df=18screen_size는 거의 안 나오는 단어라서 한 번만 등장해도 점수가 큽니다.
후보 3위: 실패 후보
score: 76.838
source_file:
outputs/godot_docs_full/pages/getting_started__first_3d_game__03.player_movement_code__6a8a15e6.md
chunk 내용:
3D player movement
Input.is_action_pressed("move_right")
Input.is_action_pressed("move_left")
Vector3.ZERO
direction.normalized()매칭된 토큰:
is_action_pressed
move_left
move_right
normalized
speed
velocity
delta
input
x
var
func
zero이건 관련 있어 보이지만 정확한 정답은 아닙니다.
왜 올라왔냐면:
Input.is_action_pressed
move_right
move_left
normalized
speed이런 이동 코드 공통 토큰이 강하게 겹치기 때문입니다.
하지만 이 문서는 3D입니다. 네 chunk는 2D이고 AnimatedSprite2D, Vector2, position.clamp, screen_size가 핵심입니다.
즉 BM25만 쓰면 이런 false positive가 생깁니다.
현재 방식이 실패하는 이유
현재 /api/retrieve에 가까운 방식은 plainto_tsquery(raw_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이러면 한 JSONL chunk 안에 너무 많은 토큰을 요구합니다.
내 시뮬레이션에서는:
strict raw AND hits: 0즉 한 chunk가 모든 unique token을 동시에 만족하지 않으면 0건입니다.
왜냐하면 실제 공식문서도 이렇게 나뉩니다.
chunk A:
Input.is_action_pressed
velocity.normalized
AnimatedSprite2D.play/stop
chunk B:
AnimatedSprite2D 설명
position.clamp
screen_size둘 다 관련 있는데, 하나의 row에 모든 것이 다 들어있지는 않습니다.
그래서 현재 방식은 갈아엎는 게 맞습니다.
방법별 대안
대안 A. 현재 방식: raw chunk + PostgreSQL plainto_tsquery
흐름:
raw chunkText
-> plainto_tsquery
-> search_tsv @@ query
-> ts_rank_cd네 chunk 결과:
실패 가능성 큼
chunk 분할이 조금만 달라져도 0건장점:
구현이 이미 되어 있음
PostgreSQL만으로 가능
인프라 단순단점:
긴 코드 chunk에 약함
AND 조건이 과함
코드 토큰 노이즈가 많음
BM25가 아님
의미 검색 불가판정:
폐기대안 B. raw chunk + BM25 only
흐름:
raw chunkText
-> tokenizer
-> BM25
-> top JSONL 반환네 chunk 결과:
1위: first_2d_game / coding_the_player
2위: first_2d_game / coding_the_player / clamp 부분
3위: first_3d_game / player_movement_code왜 성공했냐:
AnimatedSprite2D
move_right
move_left
is_action_pressed
screen_size
clamp이 단어들이 Godot docs 전체에서 희귀해서 점수를 크게 받았습니다.
장점:
원리 투명
디버깅 쉬움
모델 비용 없음
raw chunk 그대로 사용 가능
코드/API 정확 문자열에 강함단점:
동의어/설명문에 약함
API 이름이 직접 안 나오면 못 찾음
3D movement처럼 비슷한 코드가 섞임
문서가 다르게 표현하면 놓침판정:
반드시 써야 함
하지만 단독으로는 부족대안 C. raw chunk + embedding only
흐름:
raw chunkText
-> embedding vector
-> docs_chunks/api_mapping/label_prototypes embedding과 cosine search
-> top JSONL 반환모델을 쓰는 이유:
문자열이 정확히 안 겹쳐도 의미가 가까운 문서를 찾기 위해서예를 들어 query에는:
position.clamp(Vector2.ZERO, screen_size)가 있고, 문서에는:
prevent the player from leaving the screen
clamping a value means restricting it to a given range처럼 설명되어 있으면 BM25는 약해질 수 있습니다. embedding은 이런 의미 연결을 잡습니다.
장점:
표현이 달라도 찾음
문서 설명형 content에 강함
raw chunk 그대로 넣기 쉬움
긴 chunk도 처리 가능단점:
정확 API 판단이 약함
3D/2D, Godot3/Godot4 같은 미묘한 차이를 섞을 수 있음
왜 이 결과가 나왔는지 설명이 어렵다
api_mapping에서는 false positive 위험 큼네 chunk 예상:
성공:
first_2d_game / coding_the_player 찾을 가능성 높음
실패:
first_3d_game movement도 같이 높게 나올 가능성 있음
“player movement” 의미가 비슷하기 때문판정:
단독 사용 금지
BM25 후보 보완용으로 사용대안 D. raw chunk + BM25 + embedding 병렬
흐름:
raw chunkText
-> BM25 top 50
-> embedding top 50
-> 합치기
-> 점수 섞기
-> top JSONL 반환이 방식은 꽤 괜찮습니다.
네 chunk 흐름:
BM25가 잡는 것:
Input.is_action_pressed
AnimatedSprite2D
position.clamp
screen_size
embedding이 잡는 것:
player movement
2D movement tutorial
moving inside screen
animation based on movement장점:
문자열 검색과 의미 검색의 약점을 서로 보완
Qwen 없이도 1차 품질이 좋아짐
raw chunk 조건 유지단점:
점수 결합 튜닝이 필요
BM25 score와 vector score scale이 다름
false positive를 완전히 못 막음판정:
실전 최소 권장선대안 E. raw chunk + Qwen query JSON 생성 + 검색
하드코딩을 피하려면 이 방식도 가능합니다.
흐름:
raw chunkText
-> Qwen에게 검색용 JSON 생성 요청
-> 생성된 JSON으로 BM25/vector 검색
-> JSONL 후보 반환예를 들어 Qwen 출력:
{
"search_intent": "Godot 4 2D player movement tutorial",
"important_literals": [
"Input.is_action_pressed",
"Vector2.ZERO",
"AnimatedSprite2D",
"position.clamp",
"screen_size"
],
"likely_doc_topics": [
"first 2D game",
"coding the player",
"player movement",
"clamp position to screen",
"play and stop AnimatedSprite2D"
],
"migration_signals": []
}이건 Godot API signal extractor를 직접 하드코딩하지 않는 방식입니다. 대신 Qwen이 query profile을 만듭니다.
장점:
하드코딩 적음
복잡한 chunk에서 의도 요약 가능
검색어를 사람이 읽기 좋게 만들 수 있음
Godot 특화 판단을 Qwen에게 맡길 수 있음단점:
느림
비용 발생
Qwen이 없는 단서를 만들어낼 수 있음
검색 전 단계부터 hallucination 가능
반드시 raw chunk 직접근거 검증이 필요네 chunk 성공:
Qwen이 AnimatedSprite2D / Input.is_action_pressed / position.clamp를 뽑으면 성공네 chunk 실패:
Qwen이 “migration from Godot 3 to 4” 같은 없는 의도를 만들어내면 api_mapping을 잘못 끌고 옴판정:
검색 품질 실험용으로는 좋음
프로덕션 1차 검색기로는 신중대안 F. raw chunk + BM25 + embedding + reranker
이게 제일 낫습니다.
흐름:
raw chunkText
-> BM25 top 80
-> embedding top 80
-> 후보 union
-> reranker가 raw chunk와 각 JSONL 후보를 직접 비교
-> top JSONL 반환
-> Qwen validator가 직접근거 검증reranker는 query와 후보 document를 같이 읽고 관련도를 다시 매깁니다. Voyage 문서도 reranker를 embedding/BM25 같은 1차 검색 결과 후보를 받아 relevance score로 재정렬하는 모델로 설명합니다. rerank-2.5는 32K context를 가진 품질 최적화 reranker입니다. (Voyage AI)
네 chunk에서 reranker가 하는 일:
후보 1:
first_2d_game / coding_the_player
raw chunk와 직접 코드 흐름이 거의 같음
=> 매우 높음
후보 2:
same page / clamp section
position.clamp와 AnimatedSprite2D 설명이 있음
=> 높음
후보 3:
first_3d_game / player_movement
Input.is_action_pressed는 같지만 Vector3/3D 문맥
AnimatedSprite2D 없음
screen_size clamp 없음
=> 낮춤장점:
가장 품질 좋음
BM25 false positive를 많이 줄임
embedding false positive도 줄임
raw chunk 조건 유지
하드코딩 의존 낮음단점:
비용 있음
latency 있음
후보를 너무 많이 넣으면 느림
reranker도 근거 검증기는 아니라서 마지막 validator 필요판정:
최종 추천모델을 왜 쓰고 왜 안 쓰는가
BM25에는 모델이 없다
BM25는 통계 검색입니다.
이 단어가 query에 있다.
이 단어가 document에도 있다.
이 단어가 corpus에서 희귀하다.
그러면 점수를 올린다.그래서 네 chunk 같은 코드 검색에 강합니다.
AnimatedSprite2D
Input.is_action_pressed
Vector2.ZERO
position.clamp이런 건 의미보다 문자열이 중요합니다.
하지만 BM25는 “clamp가 화면 밖으로 못 나가게 한다” 같은 의미 연결은 약합니다.
embedding 모델을 쓰는 이유
embedding은 문장을 벡터로 바꿉니다.
raw chunk
-> [0.12, -0.03, ...]
docs_chunk
-> [0.11, -0.02, ...]
cosine similarity그래서 문자열이 정확히 안 겹쳐도 가까운 내용을 찾습니다.
이 프로젝트에서 embedding이 필요한 경우:
1. 문서가 설명형이라 코드와 단어가 다를 때
2. API 이름은 안 겹치지만 같은 개념일 때
3. tutorial chunk가 여러 표현으로 분산되어 있을 때
4. BM25가 0건일 때 fallback이 필요할 때embedding을 배제해야 하는 경우:
1. api_mapping source_api 정확 매칭
2. Godot3/Godot4 버전 판정
3. target_api만 맞는 false positive 제거
4. migration rule 확정즉:
embedding = recall 확장
BM25/exact = 직접근거
reranker/Qwen validator = 후보 정리와 검증모델별 특성
voyage-code-3
이 프로젝트에 제일 맞습니다.
이유:
query가 자연어 질문이 아니라 GDScript code chunk다.
검색 대상 JSONL에도 code_blocks, API 이름, 문서 설명이 섞여 있다.Voyage 공식 문서 기준 voyage-code-3는 code retrieval 최적화 모델이고, 32K context, 기본 1024차원, 256/512/1024/2048 차원을 지원합니다. Voyage 발표에서도 32개 code retrieval dataset에서 OpenAI text-embedding-3-large와 CodeSage-large 대비 높은 평균 성능을 냈다고 설명합니다. (Voyage AI)
특성:
장점:
code query에 강함
긴 chunk 처리 가능
1024/2048 선택 가능
code -> docs 검색에 적합
단점:
외부 API 의존
비용 발생
Godot 특화 모델은 아님추천 사용:
voyage-code-3 1024 float품질만 보면 2048도 가능하지만, 1024 + reranker가 더 현실적입니다.
OpenAI text-embedding-3-large
범용 고품질 embedding입니다.
OpenAI 공식 문서 기준 text-embedding-3-large는 기본 3072차원이고, text-embedding-3-small은 기본 1536차원입니다. (OpenAI 개발자)
특성:
장점:
범용 semantic retrieval 강함
문서 설명 검색에 안정적
OpenAI ecosystem 좋음
단점:
code retrieval 전용은 아님
3072차원이라 저장/인덱스 비용 큼
이 프로젝트의 “GDScript chunk -> JSONL”에는 voyage-code-3보다 덜 직접적추천도:
2순위Gemini Embedding
Google Gemini embedding은 기본 3072차원이며, output_dimensionality로 768/1536/3072 같은 크기를 선택할 수 있습니다. Google 문서도 작은 차원을 쓰면 저장공간과 계산비용을 줄이면서 품질 손실을 작게 가져갈 수 있다고 설명합니다. (Google AI for Developers)
특성:
장점:
범용/다국어 semantic retrieval 강함
문서 설명형 retrieval에 좋음
차원 축소 가능
단점:
code retrieval 전용 선택지는 아님
API 코드 정확성보다 의미 유사성 쪽
Godot migration exact 판단에는 단독 사용 위험추천도:
문서 QA 중심이면 좋음
코드 chunk 검색 중심이면 voyage-code-3 아래Jina embeddings v4
Jina embeddings v4는 복잡한 문서 검색, 다국어, multimodal, 표/차트/이미지 같은 visually rich document retrieval에 강점을 둔 모델입니다. Jina 설명 기준으로 긴 입력과 multimodal 문서 retrieval을 강조합니다. (jina.ai)
특성:
장점:
문서 검색 폭이 넓음
multimodal/복합 문서에 강함
code adapter 계열도 있음
단점:
네 프로젝트는 현재 markdown/code 중심
이미지/표 retrieval이 핵심이 아님
과한 선택일 수 있음추천도:
지금 당장은 우선순위 낮음Qwen을 검색에 쓰는 경우
Qwen은 네 말대로 현재 JSONL 생성/검증 쪽입니다. 검색에도 쓸 수는 있습니다.
사용 방식은 두 가지입니다.
1. Qwen query profile 생성기
2. Qwen validator / rerankerQwen query profile 생성기
raw chunk
-> Qwen이 검색용 JSON 생성
-> BM25/vector 검색장점:
하드코딩 감소
Godot 문맥 추론 가능
복잡한 코드 chunk를 요약 가능단점:
hallucination 가능
검색 전 단계에서 잘못된 의도 주입 가능
느림
비용 있음Qwen validator
raw chunk
+ retrieved JSONL
-> 이 JSONL이 직접근거인지 판정이건 강력합니다. 프로젝트 관찰 문서에도 “넓은 주제 유사성 말고 JSONL 필드와 chunk 문자열이 직접 맞아야 한다”는 기준이 이미 잡혀 있습니다.
추천:
Qwen은 검색 전 query 생성보다,
검색 후 direct evidence validator로 쓰는 게 더 안전함.네 chunk 기준 성공/실패 흐름
성공 흐름: BM25 + embedding + reranker
입력:
raw chunk 그대로1차 BM25가 잡음:
first_2d_game / coding_the_player
근거:
Input.is_action_pressed
move_right
move_left
velocity.normalized
AnimatedSprite2D.play
AnimatedSprite2D.stop2차 BM25가 잡음:
same page / clamp section
근거:
AnimatedSprite2D
position.clamp
Vector2.ZERO
screen_sizeembedding이 보완:
player movement
2D movement
animation based on movement
screen boundsreranker가 정리:
1위:
first_2d_game / coding_the_player
2위:
same page / clamp and AnimatedSprite2D explanation
낮춤:
first_3d_game / player_movement_code최종 JSONL:
{"table":"docs_chunks","payload":{"source_file":"outputs/godot_docs_full/pages/getting_started__first_2d_game__03.coding_the_player__161d377b.md","doc_type":"tutorial","heading":"Coding the player","content":"...Input.is_action_pressed...velocity.normalized...$AnimatedSprite2D.play()...$AnimatedSprite2D.stop()...","api_symbols":["Input.is_action_pressed","Vector2.ZERO","AnimatedSprite2D"]}}{"table":"docs_chunks","payload":{"source_file":"outputs/godot_docs_full/pages/getting_started__first_2d_game__03.coding_the_player__161d377b.md","doc_type":"tutorial","heading":"Coding the player","content":"...position += velocity * delta...position = position.clamp(Vector2.ZERO, screen_size)...","api_symbols":["Vector2.ZERO","position.clamp","AnimatedSprite2D"]}}이게 성공입니다.
실패 흐름 1: 현재 raw tsquery
입력:
raw 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결과:
0건 가능실패 이유:
한 JSONL chunk 안에 모든 단어가 없으면 실패이건 갈아엎어야 합니다.
실패 흐름 2: BM25 only
결과:
1위 first_2d_game / coding_the_player
2위 same page / clamp section
3위 first_3d_game / player_movement_code실패 이유:
first_3d_game도 Input.is_action_pressed, move_left, move_right, normalized를 공유함BM25는 “2D인지 3D인지”를 완벽하게 이해하지 않습니다. 그냥 단어 점수를 계산합니다.
그래서 BM25 only는 충분하지 않습니다.
실패 흐름 3: embedding only
결과 예상:
2D player movement
3D player movement
scripting player input
vector math
animation player docs실패 이유:
의미상 모두 player movement라 가까움embedding은 의미가 가까운 것을 잘 찾지만, API 정확도는 약합니다.
실패 흐름 4: Qwen query profile only
Qwen이 이렇게 잘 뽑으면 성공합니다.
{"important_terms":["Input.is_action_pressed","AnimatedSprite2D","position.clamp","Vector2.ZERO"]}하지만 이렇게 뽑으면 실패합니다.
{"migration_intent":"Godot 3 to Godot 4 migration","important_terms":["AnimatedSprite","AnimatedSprite2D"]}실패 이유:
raw chunk에는 AnimatedSprite가 없고 AnimatedSprite2D만 있음
Godot 4 정상 코드인데 migration mapping을 끌고 올 수 있음그래서 Qwen은 검색 전 query 생성기로만 쓰면 위험합니다. 검색 후 validator가 더 안전합니다.
최종 추천안 3개
1안: 가장 낮은 복잡도
raw chunk
-> BM25
-> top JSONL사용:
PoC 1차
빠르게 눈으로 검색 품질 확인장점:
간단함
빠름
투명함
모델 비용 없음단점:
false positive 있음
의미 검색 안 됨
문서 표현이 다르면 놓침네 chunk에서는:
성공은 함
하지만 3D movement가 섞임2안: 균형형
raw chunk
-> BM25 top 50
-> embedding top 50
-> union
-> weighted score
-> JSONL 반환
-> Qwen direct-evidence 검증사용:
현재 프로젝트에 현실적으로 제일 좋은 중간 단계장점:
BM25와 embedding 보완
Qwen으로 무관 JSONL 제거
하드코딩 적음단점:
점수 튜닝 필요
reranker 없으면 ranking이 약간 흔들림네 chunk에서는:
first_2d_game이 안정적으로 상위
3D movement는 Qwen 검증에서 낮아짐3안: 품질 최우선
raw chunk
-> BM25 top 80
-> voyage-code-3 embedding top 80
-> union
-> rerank-2.5
-> Qwen direct-evidence validator
-> 최종 JSONL사용:
최종 추천장점:
제일 품질 좋음
하드코딩 최소화
raw chunk 조건 유지
BM25/embedding false positive를 reranker가 줄임
Qwen validator가 직접근거 없는 JSONL 제거단점:
비용
latency
구현 단계가 많음네 chunk에서는:
최종 accept:
docs_chunks / first_2d_game / coding_the_player
최종 reject 또는 lower-rank:
first_3d_game / player_movement_code
unrelated api_mapping
migration label_prototypes최종 판단
내가 지금 이 프로젝트를 갈아엎는다면 이렇게 갑니다.
입력은 계속 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 직접근거 검증이 정답입니다.
Godot API signal extractor는 필수로 넣지 않는 게 맞습니다. 넣더라도 “점수 보너스” 정도로만 써야 합니다. 기본 검색은 raw chunk 기반 BM25/vector로 가는 게 네 의도에 맞습니다.