/** * 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'] ); } } } } } } } } Odoo データベース検索のサービス設計 – Raqqa

Odoo データベース検索のサービス設計

システム構成と特長(description付き)

① 決まり文句辞書(自然文 → SQL構文 + メタ情報)

  • 「自然言語の質問」と、それに対応する OdooのSQL文 のペアを登録・管理
  • 各ペアには以下の情報を付与し、検索精度と業務理解を向上:
    • question: 実際に使われる自然文(例:「田中株式会社の最新の見積」)
    • sql: 対応するSQL文(SELECT限定)
    • description: このSQLが業務上どのような意味を持つかを記載(例:「顧客の見積履歴の中で最新1件を取得」)
    • parameters: 必要な入力項目(例:顧客名、期間など)を明示 → ユーザーに聞き返すときに活用
    • tags: 「見積」「在庫」「顧客」などの分類ラベルで検索性アップ

② 検索エンジン(類似表現対応 + description活用)

  • ユーザーが入力した自然文と辞書を照合し、最も意味が近いSQLを選出
  • マッチングには自然文・description・タグの情報を複合的に活用
  • 将来的にはLangChainによる意味検索(ベクトル検索)への対応も可能

③ 実行エンジン(Odoo DBとの連携)

  • 辞書から得られたSQLをOdooのデータベースに対して実行(読み取り専用
  • 出力結果は整形され、表形式またはグラフ形式で表示可能(Matabase等との連携も想定)

③’ マッチなし時の対応(ナレッジ拡張)

  • 辞書に一致する質問がない場合は、ユーザーの問いと意図を記録
  • 導入企業に通知が届き、担当者が自然文・SQL・descriptionを登録
  • 次回からはその検索が自動的に実行可能になり、辞書が進化
  • このプロセスを通じて、導入企業にはOdooテーブル構造に関する知見が蓄積されていく

④ ナレッジ辞書としての管理

  • 辞書はSQL・自然文・description・tagsにより業務知識として体系化
  • 検索ログ・利用頻度も管理でき、よく使う業務照会の可視化が可能
  • 複数部門・拠点間でのナレッジ共有にも活用可能

フロー図

なぜこのステップが重要なのか?

課題このステップでどう解決するか
💬 曖昧な質問(例:「A商品の動き教えて」)「どの期間?」「売上?在庫?」とAIが補足質問
📊 データ構造をユーザーが知らない「この情報は sale_order_line テーブルを使います」とAIが提案
🧨 誤ったSQLや危険なクエリが実行されるリスクユーザーが確認してからSQL実行に進める(セーフティステップ)

✅ 概要:段階的進化型データ検索サービス

フェーズ目的機能
🟢 フェーズ1(導入初期〜)Odooに自然文で照会できるようにする決まり文句検索+辞書蓄積型システム
🟡 フェーズ2(導入半年〜1年)実際の検索行動からニーズ・データパターンを分析よく使う項目・テーブル・条件の抽出
🔵 フェーズ3(1年後〜)より自由で高速な分析環境へ拡張Snowflakeへの部分的データ同期+自由検索を実現

なぜこのステップが優れているか

観点フェーズ1→3戦略のメリット
ユーザー適応最初は操作が簡単で安心 → 少しずつ「考えて聞く力」が育つ
データ最適化無駄なテーブルやフィールドをSnowflakeに移さなくて済む(実用重視)
コスト最適化Odooだけで完結する軽量システムから始め、高コストなDWHを後から展開
学習効果実際の「検索行動=意思決定パターン」そのものが、システム要件の材料になる
提案力向上Odooの制限で困っているケースを収集 → Snowflakeが必要な根拠になる

学習フェーズの位置づけ(フェーズ2)

🔍 収集するユーザーデータ例

データ種別意図
検索された自然文(頻度・再利用回数)どんな質問が多いか、汎用性があるか
実行されたSQL(テーブル、列、WHERE句)実際に使われているデータ構造の傾向
マッチしなかった質問の記録Odooだけでは表現しづらいニーズの特定
補完されたパラメータユーザーがいつも選んでいる条件(顧客名、期間など)
タグの使用状況検索パターンをジャンル分け(売上、見積、在庫、納期 etc.)

❄️ Snowflakeへの展開(フェーズ3)

💡 ポイント:

  • 利用頻度が高いテーブルだけをSnowflakeにミラーリング(ストレージ最適化)
  • 検索文・タグ単位で「分析系質問」を分類し、自由度の高いSQL構文に変換
  • 自由検索モードは
    • ChatGPT + LangChain + SQLDatabaseChain(Snowflake接続)
    • Metabaseと統合して展開

🧩 移行対象の絞り込み例

選定条件説明
利用頻度Top20のSQLで使われたテーブル業務で本当に使われている領域に限定
WHERE句で頻出のフィールドパーティショニングやインデックス最適化にも活用
JOINされる相手テーブル関連性の高いものだけペアで移行

💬 訴求ポイント

  • ✅ Odooの導入と同時に、**“検索の文化”**を根付かせる
  • ✅ 導入後の行動データをもとに、段階的に分析環境を進化
  • ✅ 初期はOdooだけ、DWH投資は1年後の実需ベースで判断
  • ✅ ユーザーの質問行動そのものが社内ナレッジの資産化


Comments

コメントを残す

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