idea_world_labDEV JOURNAL
2026年6月28日日曜日

結論から

そうです。私の意図を間違えて捉えてしまいました。

正確にもう一度捉えると、これです。

1. zip には markdown 原本、スキーマ、デバッガ、設計文書が含まれています。  
2. あなたのローカルにはすでに markdown → JSONL → DB 変換版が入っています。  
3. 私が見ることができるのは「変換前の markdown と DB スキーマ/検索フロー」です。  
4. したがって PoC は zip 内の markdown を JSONL チャンクのように分割し、あなたのローカル DB に入っていたであろう検索対象をシミュレートする必要があります。  
5. あなたが求めているのはコードの骨格ではなく、チャンクが入ったときに各検索方式がどのようにスコアを生成し、なぜ成功/失敗するのかというフローを追跡することです。

そして重要な修正があります。

現在のプロジェクトの /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 が文書長の正規化オプションを受け取る ranking 関数であると説明しています。plainto_tsquery は入力文をトークン化して句読点を無視し、ストップワードでないものをクエリにします。(PostgreSQL)


結論から

あなたの条件を満たす最善はこれです。

入力:
  raw chunkText そのまま

Retriever 内部:
  1. raw chunk そのまま BM25 候補検索
  2. raw chunk そのまま code embedding 候補検索
  3. 候補 union
  4. reranker で再排序
  5. Qwen または validator が JSONL 直接根拠検証

Godot APIシグナル抽出を必須としないバージョンです。

つまり、私が以前言った「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 の1行を丸ごと 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)

このチャンクをコードトークナイザーで見ると、ユニークトークンはこのようになります。

_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個のトークン、ユニークは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は大まかに「クエリ単語がドキュメントにどれだけうまく埋め込まれているか」を計算します。

公式的には三つが核心です。

TF: このドキュメント内で単語が何回出現したか  
IDF: 全体コーパスでこの単語がどれほど稀であるか  
Length normalization: 長いドキュメントが必ず有利にならないように補正

ElasticsearchもBM25をデフォルトの関連性アルゴリズムとして使用し、用語頻度、逆文書頻度、フィールド長正規化を主要な要素として説明します。 (Elastic)

式はこのような感じです。

score(query, doc)
= Σ over query terms [
    IDF(term)
    *
    TF_boost(term frequency in doc, doc length)
  ]

つまり animatedsprite2d のような珍しい単語がドキュメントに複数回出現すると、スコアが大幅に上がります。


実際にシミュレーションした結果

zip の中の実際の Godot markdown 原本:

outputs/godot_docs_full/pages/

これを JSONL docs_chunks のように 1800文字前後のチャンクに分割し、元の raw チャンクをそのままクエリとして BM25 を実行しました。

候補 1位

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 個のチャンクのうち 18 個にだけ出現します  
move_leftは 13 個にだけ出現します  
move_rightは 15 個にだけ出現します  
is_action_pressedは 27 個にだけ出現します

それでこの単語が含まれる文書が強く上がってきます。

これがBM25が「どのように」1位を作った過程です。


候補 2位

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

このチャンクが2位である理由は、入力処理部分はありませんが、clampscreen_sizeAnimatedSprite2Dが強く合致しているからです。

特に:

screen_size df=3
clamp df=32
animatedsprite2d df=18

screen_sizeはほとんど出てこない単語なので、一度だけ出てきてもスコアが大きいです。


候補 3位: 失敗候補

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で、AnimatedSprite2DVector2position.clampscreen_sizeが核心です。

つまり BM25 だけを使うとこのような false positive が発生します。


現在の方式が失敗する理由

現在 /api/retrieve に近い方式は plainto_tsquery(raw_chunk) です。

この方式は大まかにこのようなクエリになります。

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

このようにすると、1つの JSONL チャンクで要求されるトークンが多すぎます。

私のシミュレーションでは:

strict raw AND hits: 0

つまり、あるチャンクがすべてのユニークトークンを同時に満たさない場合は0件です。

なぜなら、実際の公式ドキュメントもこのように分かれています。

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

chunk B:
  AnimatedSprite2D の説明
  position.clamp
  screen_size

両方とも関連していますが、1つの行にすべてが入っているわけではありません。

なので、現在の方式はやり直すのが正しいです。


方法別代替案

代替案 A. 現在の方式: raw chunk + PostgreSQL plainto_tsquery

流れ:

raw chunkText
-> plainto_tsquery
-> search_tsv @@ query
-> ts_rank_cd

はい、チャンク結果:

失敗の可能性が高い  
チャンクの分割が少し変わるだけでも0件

利点:

実装はすでに完了しています  
PostgreSQLだけで可能です  
インフラはシンプルです

欠点:

長いコードチャンクに弱い  
AND 条件が過剰  
コードトークンのノイズが多い  
BM25ではない  
意味検索ができない

判定:

廃棄

代替案 B. raw chunk + BM25 only

流れ:

raw chunkText
-> tokenizer
-> BM25
-> top JSONL 返却

はい、チャンク結果:

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 のドキュメント全体で希少であるため、高得点を得ました。

長所:

原理が透明  
デバッグが容易  
モデルコストなし  
raw chunk をそのまま使用可能  
コード/API は正確な文字列に強い

欠点:

同義語/説明文が弱い  
API名が直接出てこないと見つけられない  
3D movementのように似たコードが混ざる  
文書が異なる表現になると見逃す

判定:

必ず書くべきです  
しかし単独では不十分

代替案 C. raw chunk + embedding only

流れ:

raw chunkText
-> 埋め込みベクトル
-> docs_chunks/api_mapping/label_prototypes 埋め込みとコサイン検索
-> 上位 JSONL を返す

モデルを使用する理由:

文字列が完全に一致しなくても意味が近い文書を探すために

例えば、クエリには:

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はこのような意味的つながりを捉えます。

利点:

表現が違っても見つける  
文書説明型コンテンツに強い  
raw chunk そのまま入れやすい  
長い chunk も処理可能

欠点:

正確なAPI判定が弱い  
3D/2D、Godot3/Godot4 のような微妙な差異を混在させることができる  
なぜこの結果が出たのか説明が難しい  
api_mappingでは false positive のリスクが大きい

はい、チャンク予測:

成功:
  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 を返す

この方法はかなり良いです。

はい、チャンクの流れ:

BM25が捕らえるもの:
  Input.is_action_pressed
  AnimatedSprite2D
  position.clamp
  screen_size

embeddingが捕らえるもの:
  player movement
  2D movement tutorial
  moving inside screen
  animation based on movement

利点:

文字列検索と意味検索の弱点を相互に補完  
Qwen がなくても一次品質が向上  
raw chunk 条件を維持

欠点:

スコア結合のチューニングが必要です  
BM25スコアとベクトルスコアのスケールが異なる  
false positive を完全に防げない

判定:

実戦最小推奨線

代替案 E. raw chunk + Qwen query JSON 生成 + 検索

ハードコーディングを避けるためにこの方法も可能です。

フロー:

-> 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 シグナルエクストラクタを直接ハードコーディングしない方法です。
代わりに Qwen がクエリプロファイルを作成します。

利点:

ハードコーディングが少ない  
複雑なチャンクで意図の要約が可能  
検索語を人が読みやすく作ることができる  
Godot 特化の判断を Qwen に任せることができる

欠点:

遅さ  
コスト発生  
Qwenがない手がかりを作り出すことができる  
検索前段階から幻覚が可能  
必ず raw chunk の直接根拠検証が必要

はい、チャンク成功:

Qwenが AnimatedSprite2D / Input.is_action_pressed / position.clamp を取得すると成功

チャンク失敗:

Qwenが「migration from Godot 3 to 4」のような存在しない意図を作り出すと、api_mappingを誤って引っ張ってきます

判定:

検索品質の実験用としては良い  
本番第一検索エンジンとしては慎重

代替案 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 と候補ドキュメントを一緒に読み取り、関連度を再評価します。Voyage 文書も reranker を embedding/BM25 のような一次検索結果の候補を受け取り、relevance score で再順位付けするモデルとして説明します。rerank-2.5 は 32K コンテキストを持つ品質最適化 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 の偽陽性を大幅に削減  
埋め込みの偽陽性も削減  
生のチャンク条件を維持  
ハードコーディングへの依存が低い

欠点:

費用あり  
レイテンシあり  
候補を入れすぎると遅くなる  
rerankerも根拠検証器ではないので最後のバリデータが必要

判定:

最終推奨

なぜモデルを使用し、なぜ使用しないのか

BM25にはモデルがない

BM25は統計的検索です。

この単語がクエリにある。  
この単語がドキュメントにもある。  
この単語がコーパスで希少である。  
それならスコアを上げる。

だから、あなたのチャンクのようなコード検索に強いです。

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

そのため、文字列が正確に重ならなくても、近い内容を探します。

このプロジェクトで埋め込みが必要な場合:

1. ドキュメントが説明的で、コードと単語が異なるとき  
2. API名は重複しないが同じ概念のとき  
3. チュートリアルチャンクが複数の表現に分散しているとき  
4. BM25が0件のときフォールバックが必要なとき

埋め込みを除外しなければならない場合:

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コードチャンクです。  
検索対象のJSONLにもcode_blocks、API名、ドキュメント説明が混在しています。

Voyage 公式ドキュメント基準 voyage-code-3 は code retrieval 最適化モデルであり、32K コンテキスト、基本 1024 次元、256/512/1024/2048 次元をサポートします。Voyage 発表でも 32 個の code retrieval データセットで OpenAI text-embedding-3-large と CodeSage-large に対して高い平均性能を示したと説明しています。(Voyage AI)

特性:

長所:
  code queryに強い
  大きなチャンクの処理が可能
  1024/2048 を選択可能
  code -> docs の検索に適している

短所:
  外部 API への依存
  コストが発生
  Godot 特化モデルではない

おすすめの使用:

voyage-code-3 1024 float

品質だけを見ると 2048 も可能ですが、1024 + reranker の方が現実的です。


OpenAI text-embedding-3-large

汎用の高品質埋め込みです。

OpenAI 公式ドキュメントによると text-embedding-3-large はデフォルトで 3072 次元、text-embedding-3-small はデフォルトで 1536 次元です。(OpenAI 開発者)

特性:

長所:
  - 汎用的なセマンティック検索が強力
  - 文書説明検索が安定的
  - OpenAI エコシステムが優れている

短所:
  - コード検索専用ではない
  - 3072 次元で保存/インデックスコストが大きい
  - このプロジェクトの “GDScript chunk -> JSONL” は voyage‑code‑3 より直接的ではない

推奨度:

第2優先順位

Gemini Embedding

Google Gemini embeddingは基本で3072次元であり、output_dimensionalityで768/1536/3072のようなサイズを選択できます。Googleのドキュメントでも、低次元を使用すると保存領域と計算コストを削減しながら、品質の損失を最小限に抑えられると説明しています。(Google AI for Developers)

特性:

長所:
  汎用/多言語セマンティック検索が強い
  文書説明型検索に適している
  次元削減が可能

短所:
  コード検索専用の選択肢ではない
  APIコードの正確性より意味的類似性重視
  Godot移行の正確な判断には単独使用は危険

推奨度:

ドキュメント QA が中心だと良いです  
コードチャンク検索が中心なら voyage-code-3 の下

Jina embeddings v4

Jina embeddings v4は、複雑な文書検索、多言語、マルチモーダル、表/チャート/画像のようなビジュアルリッチなドキュメント検索に強みを持つモデルです。Jinaの説明に基づき、長い入力とマルチモーダル文書検索を強調しています。(jina.ai)

特性:

長所:
  文書検索の範囲が広い
  マルチモーダル/複合文書に強い
  コードアダプタ系もある

短所:
  あなたのプロジェクトは現在markdown/code中心
  画像/表のリトリーバルが核心ではない
  過剰な選択になる可能性がある

推奨度:

今は優先度が低い

Qwenを検索に使う場合

Qwenはあなたの言う通り、現在はJSONL生成/検証側です。
検索にも使うことはできます。

使用方法は二つあります。

1. Qwen クエリプロファイル生成器  
2. Qwen バリデータ / リランカー

Qwen query profile ジェネレータ

raw chunk
-> Qwenが検索用JSONを生成
-> BM25/ベクトル検索

利点:

ハードコーディングの削減  
Godot の文脈推論が可能  
複雑なコードチャンクを要約可能

欠点:

hallucination 可能  
検索前段階で誤った意図注入可能  
遅い  
コストあり

Qwen validator

raw chunk
+ retrieved JSONL
-> このJSONLが直接的な根拠かどうかを判定

これは強力です。
プロジェクト観察ドキュメントにも「広いテーマの類似性ではなく、JSONLフィールドとチャンク文字列が直接一致する」必要があるという基準がすでに設定されています。

推奨:

Qwenは検索前のクエリ生成より、検索後に直接エビデンスバリデータとして使用する方が安全です。

ネ chunk 基準 成功/失敗 フロー

成功フロー: BM25 + embedding + reranker

入力:

そのままの生データ

第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

入力:

そのままの生データ

内部 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チャンク内にすべての単語がない場合は失敗

これを覆い直す必要があります。


失敗フロー 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 クエリプロファイルのみ

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を検索前のクエリ生成器としてだけ使用すると危険です。
検索後のバリデータの方が安全です。


最終提案 3つ

案1:最も低い複雑度

raw chunk
-> BM25
-> top JSONL

使用:

PoC 第1段階  
検索品質を素早く目で確認

利点:

簡潔さ  
速さ  
透明性  
モデルコストなし

欠点:

false positive が存在する  
意味検索ができない  
文書表現が異なると見逃す

はい、チャンクでは:

成功はある  
しかし3Dムーブメントが混ざっている

2案:バランス型

- BM25 上位 50
- 埋め込み 上位 50
- 結合
- 重み付けスコア
- JSONL 返却
- Qwen direct-evidence 検証

使用:

現在のプロジェクトで現実的に最も良い中間段階

利点:

BM25 と embedding の補完  
Qwen で無関係な JSONL を削除  
ハードコーディングが少ない

欠点:

スコアのチューニングが必要です  
reranker がないと ranking がやや不安定になります

はい、チャンクでは:

first_2d_gameが安定して上位  
3D movementはQwen検証で低下

案3:品質最優先

- BM25 top 80
- voyage-code-3 embedding top 80
- union
- rerank-2.5
- Qwen direct-evidence validator
- 最終 JSONL

使用:

最終推奨

利点:

最高の品質  
ハードコーディングの最小化  
rawチャンク条件を維持  
BM25/embeddingの偽陽性をリランカーが削減  
Qwenバリデータが直接根拠のないJSONLを除去

欠点:

コスト  
latency  
実装段階が多い

はい、チャンクでは:

最終受け入れ:
  docs_chunks / first_2d_game / coding_the_player

最終拒否または下位ランク:
  first_3d_game / player_movement_code
  unrelated api_mapping
  migration label_prototypes

最終判断

もし今このプロジェクトを根本的に変えるなら、こうします。

入力は引き続き chunkText のみを保持する。

1次検索は BM25 で行う。  
理由:コード/API 文字列検索に最も透明で強力だから。

2次検索は voyage‑code‑3 埋め込みで行う。  
理由:クエリがコードチャンクであるため、コード検索モデルが適しているから。

3次ソートは rerank‑2.5 で行う。  
理由:BM25 と埋め込みが取得した類似候補の中から、実際のチャンクに合致する JSONL を上位に持ってくるため。

最終検証は Qwen direct‑evidence validator で行う。  
理由:JSONL 内にチャンクと直接合致する文字列/パターンの根拠がなければ破棄しなければならないから。

一行でまとめると:

raw chunk そのまま入力 → BM25 + code embedding 並列候補生成 → reranker 再配置 → Qwen 直接根拠検証が正解です。

Godot API シグナル抽出器は必ず入れない方が良いです。
入れる場合でも「スコアボーナス」程度に留めてください。基本検索は raw chunk ベースの BM25/vector にするのがあなたの意図に合致します。