/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } AI分析システム全体 – Raqqa

AI分析システム全体


全体像(俯瞰)

[ユーザー/Dev-Portal]
      │  (自然文 or SQL)
      ▼
API /nlq/plan  ──→  SQL候補 + ガード適用 (P5)
      │
      │(検証済SQL)
      ▼
API /nlq/execute ──→ DB実行 → stats構築
      │                 │
      │                 ├─ guard/sql の時間計測 → stats.timeline
      │                 ├─ (既存) Metabase Card 作成/再利用 → stats.metabase / stats.metabase_items
      │                 └─ (任意) AI quick分析 → stats.analysis
      │
      └─(結果・stats返却)→ Dev-Portal
                           │
                           │(履歴保存要求)
                           ▼
API /nlq/history(POST) ─→ stats.* を Artifacts[] に正規化
                           │
                           └─ nlq_history に保存(artifacts, stats, created_at)
                                   │
                                   ├─ GET /nlq/history で参照(一覧/検索)
                                   └─ [常駐] metabase-gc サービス
                                         │
                                         ├─ Metabase から NLQカード一覧取得
                                         ├─ nlq_history.artifacts の id/hash と突合
                                         ├─ Dashboard配下は保護(KEEP_DASHBOARDED)
                                         └─ TTL超過かつ参照なしのみ削除(DRY_RUN可)

ステップ別フロー

A. 計画 → 実行 → 履歴保存(Artifacts化)

  1. /nlq/plan
    • 入力(自然文/SQL)から 候補SQL を返し、P5ガードを適用した validated_sql も提示。
    • ここではまだ DB や Metabase には触れません。
  2. /nlq/execute
    • ガード適用 → 実行。結果行(サンプル or フル)と共に stats を構築。
    • stats.timelineguard/sql/metabase/analysis の各フェーズ時間を記録。
    • (既存ロジック)Metabase Card 作成/再利用があれば stats.metabase(単体)または stats.metabase_items(複数)に、
      mode, resource, id, hash, url or iframe_src, reused 等を格納。
    • (任意)AI quick 分析の要約等を stats.analysis に格納。
  3. /nlq/history(POST)
    • 受け取った stats.metabase / stats.metabase_items[]Artifacts配列へ変換(kind:"metabase", mode, resource, id, hash, url, iframe_src, created_at)。
    • 既存の artifacts[] があれば追記し、重複除去して nlq_history に保存
    • DBには artifacts jsonb / stats jsonb / created_at を保持(DDL は 060_* にて追加済み)。
  4. /nlq/history(GET)
    • session_idcursor で検索・ページング。
    • 各履歴行に artifacts[] が載っているため、Dev-Portal から Metabase の可視化へ遷移できる(public/signed どちらでも)。

ポイント:重いデータ本体は保存しない。参照に必要な **可視化リンク(Artifacts)**だけを履歴へ。


B. Metabase 側のクリーンアップ(TTL清掃)

  1. metabase-gc サービス(docker-compose.gc.yml)
    • 既定では DRY_RUN=true で起動し、一定間隔(GC_INTERVAL_SECONDS)ごとに実行。
    • Metabase に対し認証(API Key / Session / Username+Password)でアクセス。
  2. 候補収集
    • /api/card で全 Card(Question)を列挙。
    • NLQカード判定(いずれか該当):
      • nameNLQ_CARD_PREFIX(例: NLQ:)で始まる
      • visualization_settings.owner="nlq"
      • descriptionowner=nlq or hash=... を含む
  3. 保護ルール
    • KEEP_DASHBOARDED=true の場合、Dashboard に配置されている Card は保護(削除対象外)。
  4. 履歴参照との突合
    • DB の nlq_history.artifacts を走査し、参照中の id / hash をメモリ集合に取り込み。
    • カードの id が一致、または hash が一致する場合は 削除対象外
  5. TTL 判定
    • created_at(なければ updated_at)から GC_TTL_DAYS 超過のみ 候補
    • DRY_RUN=false の時だけ 実削除。実行結果は JSON1行でログ出力(収集しやすい)。

これにより、孤立して古くなった一時Cardだけが減っていき、Metabase 側が散らかりません。


主要な“受け渡しデータ”と責務

接点送受信データ(主要)責務
/nlq/plan → クライアントcandidate_sql, validated_sql, display_hintガード済みの候補提示
/nlq/execute → クライアントrows, row_count, statstimeline / metabase* / analysis実行結果と付帯メタ
/nlq/history(POST) → DBartifacts[], stats(軽量サマリ), created_atArtifacts化・保存
metabase-gc → MetabaseDELETE /api/card/{id}NLQ 管理カードのTTL清掃
metabase-gc → DBSELECT artifacts参照中(id/hash)の抽出

代表的な決定ロジック

  • Card再利用:正規化後 SQL の ハッシュMetabaseLink.hash)で既存Cardを探索し、見つかれば reused:true
  • Artifacts化stats.metabase 単体も、stats.metabase_items 配列も 両方 artifacts[] に取り込む。
  • 清掃判定
    1. NLQカードか? → Yes
    2. Dashboard配下か? → Yesなら保護
    3. 履歴参照(id/hash)があるか? → Yesなら保護
    4. TTL超過か? → Yesなら削除候補(DRY_RUNで確認→本削除)

可搬性と運用ポイント

  • ENV だけで切替:public(dev)⇄ signed(prod/K8s)を METABASE_MODE で切替。
  • タイムゾーン固定TZ=Asia/Tokyo(時刻整合/TTL判定の安定化)。
  • 索引(session_id, turn_no) / (created_at) / GIN(artifacts) により検索/GCが軽量。
  • オブザーバビリティ
    • stats.timeline(フェーズ別の処理時間)
    • GC の JSON サマリ(checked, candidates, deleted など)

失敗時のふるまい(要約)

  • /nlq/execute の quick 分析失敗:HTTP 200は維持し、analysis=None・タイムライン note:"error"
  • Artifacts 生成失敗/nlq/history(POST) 側で artifacts を空配列にして保存(致命的にしない方針が安全)。
  • Metabase 認証不可:GCは errors に記録し exit code 0(ループ停止を防止)。本番は監視で検知。

これが今回の 全データフローです。
要は「/nlq/execute が stats を返す → /nlq/history がそれを Artifacts に正規化して保存 → GC が Artifacts と TTL を手がかりに Metabase を綺麗にする」という三位一体の動きになっています。


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です