1) api/app/config.py(既存:追加のみ)
目的:環境変数の受け口を追加。起動ログに Embedding 仕様を出す。
変更点
CHROMA_URL(必須)、CHROMA_API_KEY(任意)を追加。- Embedding 既定を OpenAI固定に:
EMBED_PROVIDER=openai(既定)、OPENAI_API_KEY(既存を流用)、EMBED_MODEL=text-embedding-3-small(既定)、EMBED_DIMENSIONS=1536(既定)、EMBED_BATCH_SIZE=64(既定)、CHROMA_TIMEOUT_S=10(既定)。
- 起動時ログ:
EMBED_PROVIDER/MODEL/DIMENSIONSを INFO 出力(次元不一致の早期発見)。
2) api/app/schemas/chroma_upsert.py(既存:整合チェック)
目的:OpenAPI の ChromaUpsertRequest/Result と厳密一致。
変更点
- Request:
collections?: string[],limit: int=1000 (1..5000),dry_run: bool=false、余剰プロパティ拒否。 - Response:
processed/upserted/skipped/failedとerrors[{doc_id, reason}]。 - 既に一致していれば変更なし。差異があれば既定値/範囲を調整。
3) api/app/repos/portal_chroma_doc_repo.py(既存:メソッド追加・参照列限定)
目的:queued 取得と状態更新だけを担う薄い Repo。
変更点
list_queued(collections: list[str]|None, limit: int) -> list[Row]
取得列は 固定:id, doc_id, lang, collection, doc_text, meta(+必要なら entity/natural_key)。
※ model/status/payload は一切 SELECT しない。mark_upserted(ids: list[int])、mark_failed(id: int, reason: str)を追加。- 既存の upsert ロジックは 触らない(それはフェーズGで使用済み)。
4) api/app/services/chroma_client.py(既存:簡素 upsert API を追加)
目的:Chroma へ投げる最小クライアント。
変更点
ensure_collection(name) -> CollectionHandle:存在しなければ作成。embed_and_upsert(collection, items, *, batch_size, timeout_s) -> (upserted, failed, errors[])- Embeddingはこの層で実施(OpenAI固定)。入力:
[(id, text, metadata)]。 dry_run時は埋め込みも Chroma 呼び出しも行わないで件数だけ返す。
- Embeddingはこの層で実施(OpenAI固定)。入力:
- 例外を要約して返す(
errors: [{doc_id, reason}])。再送で安定化する前提の軽いリトライ(1回)を入れてもOK。
5) api/app/services/chroma_upsert.py(新規)
目的:H のオーケストレーション本体。
変更点(実装方針)
- 入力:
collections?, limit, dry_run。 - 取得:
repo.list_queued(..., limit)。 - Chroma用IDの決定規則(idempotent):
doc_idが::を含む(=entity::natural_key系)→doc_id + '::' + langを Chroma の document.id に採用(言語でユニーク化)。- それ以外(例:sha256 64桁など既に言語込み)→
doc_idをそのまま採用。
- コレクション単位にまとめて
chroma_client.embed_and_upsert(...)を実行。dry_runの場合は実行せず件数集計のみ。 - 成功分のみ
mark_upserted、失敗分はmark_failed。 - 戻り値:
processed/upserted/skipped(=0固定)/failed/errors[]を構築。
6) api/app/routers/chroma.py(既存:/chroma/upsert ルータを実装/簡素化)
目的:入力検証 → サービス呼び出し → 結果返却(Problem変換は既存ヘルパ準拠)。
変更点
POST /chroma/upsertにてschemas.ChromaUpsertRequestを受け、services.chroma_upsert.run()を呼ぶだけに簡素化。- 例外マッピングは既存
routers/_helpers.pyの共通を利用。
変更対象(任意/後回し)
A) api/app/services/package.py(Gフェーズ最終確認のみ)
- 確認ポイント(修正が要る場合のみ最小修正)
- view_common の NK を必ず
view_common::<action_xmlid>::<target>で生成してportal_chroma_docに保存。 repo.upsert()にdoc_idを渡さない/未知カラム(model/status/payload)を渡さない。metaは dict のまま(JSONB バインド)。
- view_common の NK を必ず
H の upsert で出る
natural_key NOT NULLや「Unconsumed column names」はGの入力不備が原因のため、ここだけは見落としがないか最終確認します(修正行は最小)。
B) DB(optional cleanup)
- いまテーブルにある 余剰列(
model,status,payload)はアプリから無視します。 - 収束後にクリーンアップするなら以下のいずれか:
- DROP列(本番影響が無いことを確認のうえ)
- もしくは
CREATE VIEW portal_chroma_doc_min AS SELECT id, entity, natural_key, lang, doc_text, meta, source_hash, collection, state, last_error, created_at, updated_at, doc_id FROM portal_chroma_doc;に差し替え参照(アプリは view を見る)。
※ H-slim では 実施不要。
C) K8s(dev だけで可)
k8s/overlays/dev/patch-api-env.yamlに CHROMA_URL / EMBED_* を追加(OpenAIキーは既存 Secret 流用)。- 変更が困る場合は
kubectl set env等の都度設定でも可。
実装の肝(合意しておきたいポイント)
- idempotency:DB doc_id が「
entity::nk型」でも「sha256(entity:nk:lang)型」でも、
上記の 「::を含むなら+ '::' + lang、含まなければそのまま」 で Chroma の document.id を決定。
→ これで どちらの DDL 系でも 再実行上書きが安定します。 - dry_run:埋め込みも Chroma も呼ばない(速度&安全重視)。
- 失敗の扱い:1件ずつ部分成功を許容。
last_errorは 300字程度に要約保存。 - 余剰列回避:Repoレイヤで SELECT/UPDATE の列を固定し、未知列に触れない。
→ 「Unconsumed column names」系の事故を確実に封じます。
最小テスト(通し)
- 既存ランブックで
.../chroma/package (dry_run=false)→GET /chroma/docs?status=queued(nk が NULL でない)。 POST /chroma/upsert { collections:["portal_view_common_ja"], limit:100, dry_run:true }→processed>0,failed=0。dry_run=false→upserted増、DB のstate='upserted'が増える。- 同じリクエスト再実行でエラーなく上書き(idempotent確認)。
① Deployment が今参照している正確なタグを取得
NS=portal-dev
APP=portal-api
IMG_EXPECTED=$(kubectl -n $NS get deploy $APP \
-o jsonpath='{.spec.template.spec.containers[?(@.name=="api")].image}')
echo "Deployment expects image: $IMG_EXPECTED"
② そのタグ名でローカルにビルド(タグを IMG_EXPECTED に合わせる)
Minikube の Docker デーモンを直接使う方法が一番確実です:
# Minikube の Docker を使う(以降の docker build/push は minikube ノード内に反映)
eval $(minikube -p minikube docker-env)
# 取得したタグ(IMG_EXPECTED)でビルド
docker build -t "$IMG_EXPECTED" -f api/Dockerfile api
# 念のため存在確認
docker images | grep portal-api
すでにローカルで別タグを作っている場合は、そのタグを IMG_EXPECTED に付け替えてもOKです:
docker tag your-local-tag "$IMG_EXPECTED"
③(containerd でも確実に)ノードへ登録されているか確認
# ノード側のコンテナランタイムに登録されているか
minikube ssh -- 'sudo crictl images | grep portal-api || sudo nerdctl images | grep portal-api || docker images | grep portal-api'
※ eval $(minikube docker-env) でビルドしていれば、ここに IMG_EXPECTED が見えるはずです。
もし eval を使わない流儀で行くなら:
# ローカルでビルドしたタグをそのままノードへ配送
minikube image load "$IMG_EXPECTED"
④ (開発中は推奨)pull policy を IfNotPresent に固定
kubectl -n $NS patch deploy/$APP -p \
'{"spec":{"template":{"spec":{"containers":[{"name":"api","imagePullPolicy":"IfNotPresent"}]}}}}'
⑤ そのままロールアウト
kubectl -n $NS rollout restart deploy/$APP
kubectl -n $NS rollout status deploy/$APP
コメントを残す