idea_world_labDEV JOURNAL
2026년 6월 28일 일요일

결론부터

맞습니다. 내가 의도를 틀리게 잡았습니다.

정확히 다시 잡으면 이겁니다.

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.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

즉 네 로컬 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
  1

BM25는 강한 토큰에 높은 점수를 줍니다. 왜냐하면 문서 전체에서 드물게 나오는 단어일수록 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=18

screen_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 / reranker

Qwen 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.stop

2차 BM25가 잡음:

same page / clamp section

근거:
  AnimatedSprite2D
  position.clamp
  Vector2.ZERO
  screen_size

embedding이 보완:

player movement
2D movement
animation based on movement
screen bounds

reranker가 정리:

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로 가는 게 네 의도에 맞습니다.