すべてのプロダクト
Search
ドキュメントセンター

Hologres:ベストプラクティス:金融データ分析と取得のためのマルチモーダル AI システムの構築

最終更新日:Jan 16, 2026

データ駆動型の世界において、テキスト、画像、音声、動画、ログなどの非構造化データは、JSON などの構造化データや半構造化データとともに、企業のコアデータ資産を形成します。非構造化データは、その生のかつ多様な形式の中に、ユーザーフィードバック、契約条件、製品欠陥の画像など、広範なビジネスインサイトを含んでいます。このトピックでは、金融シナリオにおける目論見書や契約書などの PDF ファイルを取得・分析し、きめ細かな運用上の意思決定を支援する方法について説明します。

主要な機能

このベストプラクティスは、以下の Hologres の機能を使用して、非構造化 PDF データの処理と取得に焦点を当てています。

  • オブジェクトテーブル:Object Storage Service (OSS) から PDF、画像、PPT ファイルなどの非構造化データを表形式で読み取ります。

  • AI 関数:標準 SQL 構文を使用して、組み込みの大規模言語モデル (LLM) を活用した AI 関数を呼び出し、AI サービスを構築します。

    • データ変換:`Embed` および `Chunk` オペレーターを使用して、非構造化データを構造化データに変換して保存します。外部アルゴリズムを使用せずにデータを自動的にエンベディングできます。

    • データ取得と分析:ai_genai_summarize などのオペレーターを使用して、SQL で推論、要約、翻訳などの操作を実行します。

  • 動的テーブル:増分更新モードを使用して、非構造化データを自動的に処理します。このモードは増分データのみを計算するため、繰り返し計算とリソース使用量を削減します。

  • ベクトル検索:標準 SQL を使用してベクトル検索を行い、非構造化データの類似性検索やシナリオ認識を実行します。同じクエリ内でベクトル検索とスカラー検索を組み合わせることができます。

  • 全文検索:転置インデックスやトークン化などのメカニズムを使用して、非構造化データを効率的に取得します。この機能は、キーワード一致やフレーズ検索など、さまざまな取得メソッドをサポートし、より柔軟な取得を可能にします。

ソリューションのメリット

前述の主要な機能を使用することで、Hologres でのマルチモーダル AI の取得と分析には、次のメリットがあります。

  • 完全な AI データ処理フロー:データのエンベディング、チャンキング、増分処理から取得、分析までの全プロセスをカバーします。開発者は、ビッグデータシステムと同じように、簡単に AI アプリケーションを構築できます。

  • 標準 SQL による非構造化データの処理と分析:専用の開発言語や外部システムを必要とせず、純粋な SQL を使用して非構造化データを抽出し、処理します。これにより、データ処理がよりシンプルかつ効率的になり、開発者の学習曲線が低減されます。

  • より正確で、柔軟かつインテリジェントな取得:キーワード検索、セマンティック検索、マルチモーダル検索を組み合わせたハイブリッド検索パイプラインを簡単に構築し、完全一致検索から意図認識まで、あらゆるシナリオ要件に対応します。また、AI 関数を使用してユーザーの意図、セマンティック関連付け、コンテキスト推論を深く理解し、よりインテリジェントな取得を可能にします。

  • データがエクスポートされないため、より安全:外部システムにデータをエクスポートする必要はありません。このソリューションは、Hologres のさまざまなセキュリティ機能とシームレスに統合され、データセキュリティを確保します。

このトピックでは、前述の主要な機能を使用して Hologres で非構造化データを処理および取得する方法について説明します。これにより、エンタープライズレベルのマルチモーダル AI データプラットフォームを構築し、データサイロを解消し、すべてのデータの価値を解き放つことができます。

ワークフロー

このソリューションのワークフローは次のとおりです。

  1. データセットの準備

    金融データセットから PDF ファイルを OSS にアップロードします。

  2. PDF データの処理

    オブジェクトテーブルを使用して PDF ファイルのメタデータを読み取ります。次に、増分更新をサポートする動的テーブルを作成して、データのエンベディングとチャンキングを行います。また、動的テーブルにベクトルインデックスとフルテキストインデックスを構築して、後続の取得を高速化します。

  3. ai_embed オペレーターを使用して自然言語の質問をエンベディングします。次に、全文検索とベクトル検索のデュアル検索を使用して結果を取得し、並べ替えます。最後に、LLM の推論機能を使用して、類似度が最も高い回答を出力します。

事前準備

  • データ準備

    このトピックでは、ModelScope の公開 金融データセットの PDF フォルダにある 80 社の目論見書を使用します。

  • 環境準備

    1. V4.0 以降の Hologres インスタンスを購入し、データベースを作成します。

    2. AI リソースを購入する

      このトピックでは、large-96core-512GB-384GB ノードを 1 つ使用する例を挙げます。

    3. モデルをデプロイします。このソリューションのモデルと割り当てられたリソースは次のとおりです。

      モデル名

      モデルカテゴリ

      モデルの説明

      レプリカあたりの vCPU 数

      レプリカあたりのメモリ

      レプリカあたりの GPU

      レプリカ数

      to_doc

      ds4sd/docling-models

      PDF ファイルをドキュメントに変換します。

      20

      100 GB

      1 カード (48 GB)

      1

      chunk

      recursive-character-text-splitter

      ドキュメントをチャンキングします。各 PDF ファイルは大きいため、チャンキングを推奨します。

      15

      30 GB

      0 カード (0 GB)

      1

      pdf_embed

      BAAI/bge-base-zh-v1.5

      ドキュメントをエンベディングします。

      7

      30 GB

      1 カード (96 GB)

      1

      llm

      Qwen/Qwen3-32B

      LLM を使用して、プロンプトに基づいて取得したドキュメントコンテンツに対して推論を実行します。

      7

      30 GB

      1 カード (96 GB)

      1

      説明

      モデルはデフォルトのリソース割り当てを使用します。

操作手順

  1. PDF ファイルをダウンロードして OSS にアップロード

    1. Bosera-JM 14B Challenge Dataset for Finance から 80 件の目論見書 (PDF) をダウンロードします。

    2. OSS コンソールにログインし、バケットを作成し、ダウンロードした PDF ファイルをバケットパスにアップロードします。詳細については、「簡単アップロード」をご参照ください。

  2. 権限の付与

    1. RAM コンソールにログインし、RAM ロールを作成して、必要な OSS 権限を付与します。

      AliyunOSSReadOnlyAccess 権限を付与することを推奨します。

    2. RAM ロールに Hologres のログイン権限とアクセス権限を追加します。

      • Alibaba Cloud アカウント

        RAM ロールの信頼ポリシーを直接変更します。次の JSON コードをコピーして貼り付け、以下の 2 つのパラメーターを更新します。

        • Actionsts:AssumeRole に更新します。

        • Servicehologres.aliyuncs.com に更新します。

        {
          "Statement": [
            {
              "Action": "sts:AssumeRole",
              "Effect": "Allow",
              "Principal": {
                "RAM": [
                  "acs:ram::1866xxxx:root"
                ],
                "Service": [
                  "hologres.aliyuncs.com"
                ]
              }
            }
          ],
          "Version": "1"
        }
      • RAM ユーザー

        1. RAM ユーザーに権限を付与します。

          1. RAM コンソールで、[権限] > [ポリシー] に移動し、[ポリシーの作成] をクリックします。[ポリシーの作成] ページで、[JSON] タブを選択します。次の JSON コードをコピーして貼り付け、RAM ユーザーが RoleARN のユーザーマッピングを作成できるようにするカスタムポリシーを作成します。

            詳細な手順については、「カスタムポリシーの作成」をご参照ください。

            {
              "Version": "1",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": "hologram:GrantAssumeRole",
                  "Resource": "<RoleARN>"
                }
              ]
            }
          2. [ID] > [ユーザー] に移動し、対象の RAM ユーザーの [操作] 列にある [権限の追加] をクリックして、カスタムポリシーを付与します。「RAM ユーザーへの権限付与」をご参照ください。

        2. RAM ロールに権限を付与します。

          RAM ロールの信頼ポリシーを変更します。更新する主要なパラメーターは次のとおりです。

          • Action:sts:AssumeRole に更新します。

          • Service:hologres.aliyuncs.com に更新します。

          {
            "Statement": [
              {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                  "RAM": [
                    "acs:ram::1866xxxx:root"
                  ],
                  "Service": [
                    "hologres.aliyuncs.com"
                  ]
                }
              }
            ],
            "Version": "1"
          }
  3. PDF ファイルのエンベディングとチャンキング

    オブジェクトテーブルと動的テーブルを作成して、PDF メタデータを読み取り、処理します。この複数ステップのプロセスを簡素化するために、Hologres はストアドプロシージャを提供します。このストアドプロシージャは、次の機能を提供します。

    • PDF メタデータを読み取るためのオブジェクトテーブルを作成します。

    • 増分更新モードで動的テーブルの結果テーブルを作成し、処理されたデータを保存します。このテーブルには、ベクトルインデックスとフルテキストインデックスが設定されます。動的テーブルは手動でリフレッシュする必要があります。

    • 動的テーブルのリフレッシュ中に、ai_embedai_chunk を使用してデータがエンベディングおよびチャンキングされます。

    ストアドプロシージャのコードは次のとおりです。

    CALL create_rag_corpus_from_oss(
        oss_path => 'oss://xxxx/bs_challenge_financial_14b_dataset/pdf',
        oss_endpoint => 'oss-cn-hangzhou-internal.aliyuncs.com',
        oss_role_arn => 'acs:ram::186xxxx:role/xxxx',
        corpus_table => 'public.dt_bs_challenge_financial'
    );
  4. 結果テーブルのリフレッシュ

    データ変換を完了するには、オブジェクトテーブルと動的テーブルを手動でリフレッシュする必要があります。Hologres は、次の機能を提供するPDF 処理用のストアドプロシージャを提供します。

    • オブジェクトテーブルをリフレッシュして、PDF メタデータを取得します。

    • 動的テーブルをリフレッシュして、PDF のエンベディングとチャンキングを実行します。

    次のコードは、このストアドプロシージャを呼び出します。

    CALL refresh_rag_corpus_table(
        corpus_table => 'public.dt_bs_challenge_financial'
    );
  5. PDF ファイルの検索

    データが処理された後、ビジネスシナリオに基づいてベクトル検索または全文検索を実行できます。たとえば、企業の目論見書から業績トレンドを照会して、会社の将来の見通しを判断できます。この情報は、将来の投資判断に役立ちます。

    ベクトル検索

    ベクトル検索のために、Hologres は、質問のエンベディング、プロンプトの構築、LLM による回答生成などのプロセスをカプセル化したベクトル検索関数を提供します。この関数を直接呼び出して、ベクトル検索を実行できます。

    -- ベクトル検索のみ + AI による再ランキング
    SELECT qa_vector_search_retrieval(
      question => '報告期間中の 2014年、2015年、2016年における国科微電子の営業収益と純利益の前年比増加率はどのくらいですか?',
      corpus_table => 'dt_bs_challenge_financial',
      prompt => '次の業績トレンドが悲観的か楽観的かを分析し、理由を述べてください:${question}\n\n 参考情報:\n\n ${context}'
    )

    取得された回答は次のとおりです。

    qa_retrieval
    ---------
    "提供された情報に基づくと、国科微電子の業績トレンドの分析は次の結論に至ります。
    
    ### I. 業績トレンド分析:悲観的
    
    #### 1. **収益成長の鈍化**
    - 2014年の収益は前年比 **15.13%** 増加しましたが、2015年には **5.21% 減少**しました。2016年のデータは提供されていませんが、2015年に収益成長トレンドが大幅に低下したことは明らかです。
    - 2012年から 2014年までの収益の年平均成長率 (CAGR) はわずか **4.47%** であり、事業拡大が遅いことを示しています。
    
    #### 2. **純利益成長の継続的な低下**
    - 2014年の純利益は **5.43%** 増加しましたが、2015年には **3.29% 減少**しました。
    - 経常外損益を控除した後、親会社株主に帰属する純利益は 2014年に **3.14%** 減少し、2015年にはさらに **5.60%** 減少しました。これは、会社の主力事業の収益性が継続的に悪化していることを示しています。
    - 2012年から 2014年までの経常外損益控除後の純利益の CAGR は **-4.38%** でした。これは収益成長よりもはるかに低く、主力事業が収益性がなく、成長が経常外損益に依存していることを示しています。
    
    #### 3. **経常外損益の割合が高い**
    - 報告期間中、経常外損益が純利益に占める割合は高く、2014年、2013年、2012年にそれぞれ **17.54%**、**10.25%**、**8.06%** でした。これは、会社の利益の一部が、中核事業の持続的な成長からではなく、政策支援や政府補助金などの経常外要因から来ていることを示しています。
    - 利益成長を維持するために経常外損益に依存することは、会社の長期的な安定した発展には寄与しません。
    
    #### 4. **自己資本利益率 (ROE) の低下**
    - 加重平均 ROE は 2014年の **18.10%** から 2015年には **24.82%** に、そして 2016年には **28.23%** に増加しました。データは成長を示しているように見えますが、この指標は経常外損益控除後の純利益に基づいて計算されており、純利益自体は減少していることに注意してください。したがって、この成長は収益性の大幅な改善ではなく、資本構成の変化に関連している可能性があります。
    
    ### II. まとめ
    1. **主力事業の成長鈍化**:収益と純利益の両方の成長が下降傾向にあり、特に純利益の減少は収益性の弱化を示しています。
    2. **経常外損益への高い依存度**:経常外損益が会社の利益に占める割合が高く、主力事業の収益性が十分でなく、会社の業績の持続可能性に疑問があることを示しています。
    3. **激しい市場競争**:同社が購入する産業用コンピュータ、ディスプレイ、電源の市場は競争が激しく、価格は安定しており、利益率は圧迫されています。
    4. **市場環境**:ステンレス鋼市場価格と原材料価格の変動は、会社の営業収益に一定の影響を与える可能性があります。同社は影響を軽減するための措置を講じていますが、長期的には依然として注意が必要です。
    
    ### III. 結論
    全体として、国科微電子の業績トレンドは **悲観的** です。会社の主力事業の成長は鈍く、純利益は継続的に減少し、経常外損益に大きく依存しています。将来の収益性の持続可能性には疑問があります。同社は、長期的な安定した発展を達成するために、中核事業の競争力を強化し、コスト構造を最適化し、主力事業の収益性を向上させる必要があります。"

    全文検索

    全文検索のために、Hologres は、質問のエンベディング、プロンプトの構築、LLM による回答生成などのプロセスをカプセル化した全文検索関数を提供します。この関数を直接呼び出して、全文検索を実行できます。

    -- 全文検索による取得
    SELECT qa_text_search_retrieval(
        question => '報告期間中の 2014年、2015年、2016年における国科微電子の営業収益と純利益の前年比増加率はどのくらいですか?',
        corpus_table => 'dt_bs_challenge_financial',
        prompt => '次の業績トレンドが悲観的か楽観的かを分析し、理由を述べてください:${question}\n\n 参考情報:\n\n ${context}'
    );

    取得された回答は次のとおりです。

    qa_text_search_retrieval
    ----------------
    "提供された情報に基づくと、2014年、2015年、2016年の国科微電子の全体的な業績トレンドは **悲観的** です。具体的な理由は次のとおりです。
    
    ### 1. **収益成長の鈍化**
    - 2014年の収益成長率は **15.13%** でしたが、2015年には **-5.21%** に転じ、マイナス成長を示しました。
    - 2012年から 2014年までの収益の年平均成長率 (CAGR) はわずか **4.47%** であり、会社の収益成長が遅く、事業展開が力強くないことを示しています。
    - 2015年上半期の収益予測は 2014年同期とほぼ同じでしたが、2015年上半期の純利益は前年同期と比較して **わずかに減少** し、収益性の低下を示しています。
    
    ### 2. **純利益および経常外項目を除く純利益の成長が悪い**
    - 2014年の純利益成長率は **5.43%** でしたが、2015年には **-3.29%** に減少し、純利益が減少したことを意味します。
    - 経常外損益控除後の純利益の成長率は 2014年に **-3.14%** であり、2015年にはさらに **-5.60%** に減少し、会社の主力事業の収益性が継続的に低下していることを示しています。
    - 2012年から 2014年までの経常外項目を除く純利益の CAGR は **-4.38%** であり、収益の CAGR よりも大幅に低いです。これは、会社の主力事業の収益性が弱いことを示しています。
    
    ### 3. **営業活動によるキャッシュフローの変動**
    - 2014年、商品販売およびサービス提供による現金収入の営業収益に対する割合は、過去 2 年間と比較して減少しました。これは主に、一部の収益項目の **支払遅延** が原因であり、会社のキャッシュフロー管理に問題があることを示しています。
    - 2013年、商品およびサービスの購入による現金の営業費用に対する割合は比較的高かったです。これは主に、その年に **原材料が購入され、生産が完了** したが、一部の費用が 2014年まで繰り越されなかったため、2014年の割合が低くなったためです。これは、会社の調達と生産のペースが安定していなかったことを反映しています。
    
    ### 4. **投資および収益性指標**
    - 加重平均自己資本利益率 (ROE) は 2014年に **18.10%** であり、2015年には **24.82%** に上昇し、2016年にはさらに **28.23%** に増加しました。改善は見られましたが、ROE の増加は主に **財務レバレッジ** によるものであり、中核事業の収益性の改善によるものではない可能性があります。
    - 純利益および経常外項目を除く純利益の継続的な減少を考慮すると、ROE の増加は会社の業績の改善を完全には反映していません。
    
    ### 5. **2015年上半期の業績予測**
    - 2015年上半期の推定営業収益は **8,505万元から 1億395万元** であり、2024年上半期の **1億127.35万元** と同程度です。しかし、推定純利益は **2,340万元から 2,860万元** であり、2014年の **2,912.66万元** よりも低く、会社の収益性の低下を示しています。
    
    ### まとめ
    要約すると、2014年から 2016年までの国科微電子の業績トレンドは **悲観的** です。ROE は改善しましたが、収益成長の鈍化、純利益および経常外項目を除く純利益の継続的な減少、営業活動によるキャッシュフローの変動は、会社の主力事業の収益性が弱く、経営品質を向上させる必要があることを示しています。"

    ハイブリッド検索

    ベクトル検索、全文検索、ランキングを組み合わせたハイブリッド検索シナリオでは、Hologres はランキング付きハイブリッド検索関数を提供します。この関数には次の機能があります。

    • 質問に基づいてベクトル計算を使用して上位 20 件の回答を取得します。

    • 質問に基づいて全文検索を使用して上位 20 件の回答を取得します。

    • ai_rank を使用して、ベクトル検索と全文検索からの回答を並べ替え、上位 1 件の回答を出力します。

    • ai_gen と LLM を使用して、プロンプトと取得した回答に基づいて最終的な回答を生成します。

    -- 全文検索とベクトル検索のデュアル検索 + AI による再ランキング
    SELECT qa_hybrid_retrieval(
        question => '報告期間中の 2014年、2015年、2016年における湖南国科微電子股份有限公司の営業収益と純利益の前年比増加率はどのくらいですか?',
        corpus_table => 'dt_bs_challenge_financial',
        prompt => '次の業績トレンドが悲観的か楽観的かを分析し、理由を述べてください:${question}\n\n 参考情報:\n\n ${context}'
    );

    取得された回答は次のとおりです。

    qa_hybrid_retrieval
    ---
    "提供された情報に基づき、国科微電子の業績トレンドを分析し、それが悲観的か楽観的かを次のように判断できます。
    
    ---
    
    ### I. **営業収益トレンド分析**
    1. **2012-2014 年平均成長率 (CAGR)**:
       - 営業収益の CAGR は **4.47%** であり、会社の収益成長が比較的に安定していたことを示しています。
       - 2014年、営業収益は **1億8,154.06万元** で、2013年から **15.13%** 増加しました。
       - 2015年、営業収益は前年比 **5.21% 減少し**、マイナス成長を示しました。
    
    2. **結論**:
       - 2014年に収益成長は回復しましたが、2015年には大幅な減少が見られ、会社の事業拡大が何らかの抵抗に遭遇したことを示しています。
    
    ---
    
    ### II. **純利益トレンド分析**
    1. **2012-2014 CAGR**:
       - 経常外損益控除後の純利益の CAGR は **-4.38%** であり、営業収益の CAGR よりも低いです。これは、会社の収益性が低下していることを示しています。
       - 2014年、経常外項目を除く純利益は **4,273.1万元** で、2013年から **3.14%** 減少しました。
       - 2015年、経常外項目を除く純利益はさらに前年比 **5.60% 減少**しました。
    
    2. **経常外損益の影響**:
       - 純利益に占める経常外損益の割合は、2014年、2013年、2012年にそれぞれ **17.54%**、**10.25%**、**8.06%** であり、上昇傾向を示しています。
       - 経常外損益の増加は、主に政府補助金と投資収益によるものであり、主力事業からの持続的な成長によるものではありません。
    
    3. **結論**:
       - 経常外項目を除く純利益が 2 年連続で減少していることは、会社の主力事業の収益性が弱まっていることを示しており、業績成長は経常外損益に依存しており、これは懸念すべきシグナルです。
    
    ---
    
    ### III. **キャッシュフローと経営の安定性**
    1. **営業活動によるキャッシュフロー**:
       - 2014年の営業収益は **1億8,154.06万元** でしたが、商品販売およびサービス提供による現金は明確に記載されておらず、キャッシュフローが健全であるかどうかを判断することはできません。
       - 報告期間中、会社の銀行預金はそれぞれ **1億3,063.38万元**、**4,152.54万元**、**9,864.61万元** でした。流動性は大幅に変動しましたが、主要な顧客、サプライヤー、ビジネスモデルは安定していました。
    
    2. **結論**:
       - 会社のキャッシュフローは変動しますが、安定した顧客基盤とサプライヤー基盤、およびビジネスモデルは、将来の発展に一定の保証を提供します。
    
    ---
    
    ### IV. **2015年上半期の業績予測**
    - 2015年 1月から 6月までの推定営業収益は **8,505万元から 1億395万元** であり、2014年上半期の **4,641.19万元** から大幅に増加しています。
    - しかし、2015年 1月から 3月までの純利益は前年比 **48.26%** 減少し、これは主に収益が認識されたプロジェクトの粗利益率が低かったためです。
    
    ---
    
    ### V. **総合的な分析と判断**
    1. **楽観的な要因**:
       - 2014年に営業収益は急速に成長し、**15.13%** に達しました。
       - 2015年上半期には営業収益が大幅に増加すると予想されており、会社が徐々に回復している可能性を示しています。
       - 主要な顧客、サプライヤー、ビジネスモデルの安定性は、会社に良好な経営基盤を提供します。
    
    2. **悲観的な要因**:
       - 2015年、営業収益は前年比 **5.21% 減少し**、純利益も減少しました。
       - 経常外項目を除く純利益が 2 年連続で減少していることは、主力事業の収益性が不十分であることを示しています。
       - 経常外損益の割合が増加しており、業績成長は政府補助金と投資収益に依存しており、成長の勢いを欠いています。
       - 2015年 1月から 3月までの純利益は **48.26%** 急減し、短期的な業績の大きな変動を示しています。
    
    ---
    
    ### **最終結論:全体的なトレンドは悲観的**
    - 2014年に会社の営業収益は回復し、2015年上半期には成長が見込まれていますが、**経常外項目を除く純利益の継続的な減少**、**純利益成長の経常外損益への依存**、および **短期的な業績の大きな変動** は、会社の現在の業績成長が持続可能性と安定性を欠いていることを示しています。
    - したがって、長期的な視点から見ると、会社の業績トレンドは **悲観的** です。主力事業の収益性と経常外損益への依存度に注意を払う必要があります。
    
    ---
    
    ### **推奨事項**
    1. 会社の主力事業の収益性が改善されるかどうかに注意してください。
    2. 経常外損益への依存を減らし、成長の勢いを高めてください。
    3. 顧客およびサプライヤーとの関係を安定させ、事業構造を最適化し、粗利益率を高めてください。"

    RRF を使用したハイブリッド検索

    ベクトル検索と全文検索を使用した後、Reciprocal Rank Fusion (RRF) を使用して取得した結果を並べ替えることができます。便宜上、Hologres はこのプロセスをRRF を使用したハイブリッド検索関数にカプセル化しています。詳細については、付録をご参照ください。この関数には次の機能があります。

    • 質問に基づいてベクトル計算を使用して上位 20 件の回答を取得します。

    • 質問に基づいて全文検索を使用して上位 20 件の回答を取得します。

    • ベクトル検索と全文検索からの回答の RRF スコアを計算し、上位 N 件の回答を出力します。

    • ai_gen と LLM を使用して、プロンプトと取得した回答に基づいて最終的な回答を生成します。

    -- 全文検索とベクトル検索のデュアル検索 + RRF による再ランキング
    SELECT qa_hybrid_retrieval_rrf(
        question => '報告期間中の 2014年、2015年、2016年における国科微電子の営業収益と純利益の前年比増加率はどのくらいですか?',
        corpus_table => 'dt_bs_challenge_financial',
        prompt => '次の業績トレンドが悲観的か楽観的かを分析し、理由を述べてください:${question}\n\n 参考情報:\n\n ${context}'
    );

    取得された回答は次のとおりです。

    qa_hybrid_retrieval_rrf
    ------------------
    "提供された情報に基づくと、国科微電子の業績トレンドの分析は次の結論に至ります。
    
    ### **業績トレンドの判断:悲観的**
    #### **理由は次のとおりです。**
    
    1. **純利益の成長が収益の成長を下回っている:**
       - 提供された情報によると、会社の **2012年から 2014年までの営業収益の年平均成長率 (CAGR) は 4.47%** であり、全体的な事業成長が比較的に安定していたことを示唆しています。
       - しかし、**経常外損益控除後の親会社株主に帰属する純利益の CAGR は -4.38%** であり、収益成長率を大幅に下回っています。これは、収益が成長している一方で、会社の収益性が同調して改善されず、むしろ低下したことを示しています。これは、コストの上昇、粗利益率の低下、または経常外損益の減少などの要因による可能性があります。
    
    2. **純利益の大幅な変動:**
       - 2015年 1月から 3月までの純利益は前年比で 48.26% 減少しました。主な理由は、収益が認識されたプロジェクト(主にモジュールの外部委託に基づいていた無錫地下鉄 1 号線プロジェクトなど)の粗利益率が低かったためです。これは、会社の短期的な業績が事業構造の変化に影響されやすく、ある程度の不安定さを示していることを示しています。
    
    3. **粗利益率と収益性の低下:**
       - 「会社の純利益に対する主力事業利益の貢献度」が 2014年には 2013年と比較してわずかに減少し、2013年は 2012年よりも低かったと述べられています。これは、会社のコアビジネスの収益性が弱まっている可能性があり、市場競争の激化、コストの上昇、または製品構造の変化が原因である可能性があります。
    
    4. **2015年上半期の利益減少予測:**
       - 2015年上半期の推定営業収益は 8,505万元から 1億395万元で、2014年上半期とほぼ同水準です。しかし、推定純利益は 2,340万元から 2,860万元で、2014年上半期の 2,912.66万元よりも低いです。これは、会社の収益性がさらに低下しており、特定の経営圧力に直面している可能性があることを示しています。
    
    5. **事業成長の鈍化:**
       - 収益成長は安定していますが、純利益の減少は、会社の事業成長の質が高くなく、効果的に利益に転換されていないことを示しています。これは、会社の将来の発展に対する投資家の信頼に影響を与える可能性があります。
    
    ### **まとめ:**
    国科微電子の全体的な業績トレンドは **悲観的** です。営業収益は安定した成長を維持していますが、純利益の成長は大幅に遅れているか、マイナス成長さえ示しています。これは、会社の収益性が低下しており、事業開発の質が高くなく、短期的な業績変動のリスクがあることを示しています。会社が効果的に粗利益率を上げ、コストを管理し、製品構造を最適化できなければ、将来の業績は引き続き圧迫される可能性があります。"

付録:関数とプロシージャの定義

このトピックで使用されるストアドプロシージャと関数の定義を参考に提供します。

説明

Hologres では関数を作成できません。以下のプロシージャと関数は参照用です。変更して実行することはできません。

PDF 処理用のストアドプロシージャ

  • オブジェクトテーブルと動的テーブルの作成

    CREATE OR REPLACE PROCEDURE create_rag_corpus_from_oss(
        oss_path TEXT,
        oss_endpoint TEXT,
        oss_role_arn TEXT,
        corpus_table TEXT,
        embedding_model TEXT DEFAULT NULL,
        parse_document_model TEXT DEFAULT NULL,
        chunk_model TEXT DEFAULT NULL,
        chunk_size INT DEFAULT 300,
        chunk_overlap INT DEFAULT 50,
        overwrite BOOLEAN DEFAULT FALSE
    )
    AS $$
    DECLARE
        corpus_schema TEXT;
        corpus_name TEXT;
        obj_table_name TEXT;
        full_corpus_ident TEXT;
        full_obj_ident TEXT;
        embed_expr TEXT;
        chunk_expr TEXT;
        parse_expr TEXT;
        embedding_dims INT;
    BEGIN
        -- 1. スキーマ名とテーブル名を分割します。
        IF position('.' in corpus_table) > 0 THEN
            corpus_schema := split_part(corpus_table, '.', 1);
            corpus_name   := split_part(corpus_table, '.', 2);
        ELSE
            corpus_schema := 'public';
            corpus_name   := corpus_table;
        END IF;
    
        obj_table_name := corpus_name || '_obj_table';
    
        full_corpus_ident := format('%I.%I', corpus_schema, corpus_name);
        full_obj_ident    := format('%I.%I', corpus_schema, obj_table_name);
        
        -- 2. 上書きが必要な場合は、まずテーブルとインデックスを削除します。
        IF overwrite THEN
            DECLARE
                dyn_table_exists BOOLEAN;
                rec RECORD;
            BEGIN
                -- 動的テーブルが存在するかどうかを確認します。
                SELECT EXISTS (
                    SELECT 1
                    FROM pg_class c
                    JOIN pg_namespace n ON n.oid = c.relnamespace
                    WHERE c.relname = corpus_name
                    AND n.nspname = corpus_schema
                )
                INTO dyn_table_exists;
    
                IF dyn_table_exists THEN
                    -- 2.1 動的テーブルの自動リフレッシュを無効にします。
                    -- RAISE NOTICE 'Disabling auto refresh for %', full_corpus_ident;
                    -- EXECUTE format('ALTER TABLE IF EXISTS %s SET (auto_refresh_enable=false)', full_corpus_ident);
    
                    -- 2.2 実行中のリフレッシュタスクを見つけてキャンセルします。
                    FOR rec IN
                        EXECUTE format(
                            $f$
                            SELECT query_job_id
                                FROM hologres.hg_dynamic_table_refresh_log(%L)
                                WHERE status = 'RUNNING';
                            $f$,
                            corpus_table
                        )
                    LOOP
                        RAISE NOTICE 'Found running refresh job: %', rec.query_job_id;
                        IF hologres.hg_internal_cancel_query_job(rec.query_job_id::bigint) THEN
                            RAISE NOTICE 'Cancel job % succeeded.', rec.query_job_id;
                        ELSE
                            RAISE WARNING 'Cancel job % failed.', rec.query_job_id;
                        END IF;
                    END LOOP;
    
                    -- 2.3 動的テーブルを削除します。
                    EXECUTE format('DROP TABLE IF EXISTS %s;', full_corpus_ident);
                ELSE
                    RAISE NOTICE 'Dynamic table % does not exist, skip cancel job and drop.', full_corpus_ident;
                END IF;
    
                -- 2.4 いずれの場合も、オブジェクトテーブルは削除する必要があります。
                EXECUTE format('DROP OBJECT TABLE IF EXISTS %s;', full_obj_ident);
            END;
        END IF;
    
        -- 3. オブジェクトテーブルを作成します。
        RAISE NOTICE 'Create object table: %', obj_table_name;
        EXECUTE format(
            $f$
            CREATE OBJECT TABLE %s
            WITH (
                path = %L,
                oss_endpoint = %L,
                role_arn = %L
            );
            $f$,
            full_obj_ident,
            oss_path,
            oss_endpoint,
            oss_role_arn
        );
    
        COMMIT;
    
        -- 4. オブジェクトテーブルをリフレッシュします。
        RAISE NOTICE 'Refresh object table: %', obj_table_name;
        EXECUTE format('REFRESH OBJECT TABLE %s;', full_obj_ident);
    
        COMMIT;
    
        -- 5. ドキュメント解析モデルを選択します。
        IF parse_document_model IS NULL OR length(trim(parse_document_model)) = 0 THEN
            parse_expr := 'ai_parse_document(file, ''auto'', ''markdown'')';
        ELSE
            parse_expr := format(
                'ai_parse_document(%L, file, ''auto'', ''markdown'')',
                parse_document_model
            );
        END IF;
    
        -- 6. チャンキングモデルを選択します。
        IF chunk_model IS NULL OR length(trim(chunk_model)) = 0 THEN
            chunk_expr := format('ai_chunk(doc, %s, %s)', chunk_size, chunk_overlap);
        ELSE
            chunk_expr := format(
                'ai_chunk(%L, doc, %s, %s)',
                chunk_model,
                chunk_size,
                chunk_overlap
            );
        END IF;
    
        -- 7. エンベディングモデルを選択します。
        IF embedding_model IS NULL OR length(trim(embedding_model)) = 0 THEN
            embed_expr := 'ai_embed(chunk)';
    
            EXECUTE 'SELECT array_length(ai_embed(''dummy''), 1)'
            INTO embedding_dims;
        ELSE
            embed_expr := format('ai_embed(%L, chunk)', embedding_model);
    
            EXECUTE format(
                'SELECT array_length(ai_embed(%L, ''dummy''), 1)',
                embedding_model
            )
            INTO embedding_dims;
        END IF;
    
        RAISE NOTICE 'embedding dimension is: %', embedding_dims;
    
        -- 8. RAG 出力を保存するための動的テーブルを作成します。
        RAISE NOTICE 'create dynamic table: %', corpus_name;
        EXECUTE format(
            $f$
            CREATE DYNAMIC TABLE %s(
                CHECK(array_ndims(embedding_vector) = 1 AND array_length(embedding_vector, 1) = %s)
            )
            WITH (
                vectors = '{
                  "embedding_vector": {
                    "algorithm": "HGraph",
                    "distance_method": "Cosine",
                    "builder_params": {
                    "base_quantization_type": "sq8_uniform",
                    "max_degree": 64,
                    "ef_construction": 400,
                    "precise_quantization_type": "fp32",
                    "use_reorder": true
                    }
                  }
                }',
                auto_refresh_mode = 'incremental',
                freshness = '5 minutes',
                auto_refresh_enable = 'false'
            ) AS
            WITH parsed_doc AS (
                SELECT object_uri,
                       etag,
                       %s AS doc
                  FROM %s
            ),
            chunked_doc AS (
                SELECT object_uri,
                       etag,
                       unnest(%s) AS chunk
                  FROM parsed_doc
            )
            SELECT
                object_uri,
                etag,
                chunk,
                %s AS embedding_vector
              FROM chunked_doc;
            $f$,
            full_corpus_ident,
            embedding_dims,
            parse_expr,
            full_obj_ident,
            chunk_expr,
            embed_expr
        );
        COMMIT;
    
        -- 9. 全文検索インデックスを作成します (インデックス名 = テーブル名 || '_fulltext_idx')。
        EXECUTE format(
            'CREATE INDEX %I ON %s USING FULLTEXT (chunk);',
            corpus_name || '_fulltext_idx',
            full_corpus_ident
        );
    
        RAISE NOTICE '';
        RAISE NOTICE 'Create RAG corpus success to table: %', corpus_table;
        RAISE NOTICE '    Vector index is: %.embedding_vector', corpus_table;
        RAISE NOTICE '    TextSearch index is: %.chunk', corpus_table;
    END;
    $$ LANGUAGE plpgsql;
  • オブジェクトテーブルと動的テーブルをリフレッシュするストアドプロシージャ

    CREATE OR REPLACE PROCEDURE refresh_rag_corpus_table(
        corpus_table TEXT
    )
    AS $$
    DECLARE
        corpus_schema TEXT;
        corpus_name   TEXT;
        obj_table_name TEXT;
        full_corpus_ident TEXT;
        full_obj_ident    TEXT;
    BEGIN
        -- 1. スキーマとテーブル名を解析します。
        IF position('.' in corpus_table) > 0 THEN
            corpus_schema := split_part(corpus_table, '.', 1);
            corpus_name   := split_part(corpus_table, '.', 2);
        ELSE
            corpus_schema := 'public';
            corpus_name   := corpus_table;
        END IF;
    
        obj_table_name := corpus_name || '_obj_table';
    
        full_corpus_ident := format('%I.%I', corpus_schema, corpus_name);
        full_obj_ident    := format('%I.%I', corpus_schema, obj_table_name);
    
        -- 2. オブジェクトテーブルをリフレッシュします。
        RAISE NOTICE 'Refreshing Object Table: %', obj_table_name;
        EXECUTE format('REFRESH OBJECT TABLE %s;', full_obj_ident);
    
        -- 3. 動的テーブルをリフレッシュします。
        RAISE NOTICE 'Refreshing Dynamic Table: %', corpus_name;
        EXECUTE format('REFRESH TABLE %s;', full_corpus_ident);
    
        RAISE NOTICE 'Refresh complete for corpus table %', corpus_table;
    END;
    $$ LANGUAGE plpgsql;
  • オブジェクトテーブルと動的テーブルを削除するストアドプロシージャ

    CREATE OR REPLACE PROCEDURE drop_rag_corpus_table(
        corpus_table TEXT
    )
    AS $$
    DECLARE
        corpus_schema TEXT;
        corpus_name   TEXT;
        obj_table_name TEXT;
        full_corpus_ident TEXT;
        full_obj_ident    TEXT;
        rec RECORD;
    BEGIN
        -- 1. スキーマとテーブル名を解析します。
        IF position('.' in corpus_table) > 0 THEN
            corpus_schema := split_part(corpus_table, '.', 1);
            corpus_name   := split_part(corpus_table, '.', 2);
        ELSE
            corpus_schema := 'public';
            corpus_name   := corpus_table;
        END IF;
    
        obj_table_name := corpus_name || '_obj_table';
    
        full_corpus_ident := format('%I.%I', corpus_schema, corpus_name);
        full_obj_ident    := format('%I.%I', corpus_schema, obj_table_name);
    
        -- 2. テーブルを削除します。
        -- 2.1 動的テーブルの自動リフレッシュを無効にします。
        -- RAISE NOTICE 'Disabling auto refresh for %', full_corpus_ident;
        -- EXECUTE format('ALTER TABLE IF EXISTS %s SET (auto_refresh_enable=false)', full_corpus_ident);
    
        -- 2.2 実行中のリフレッシュタスクを見つけてキャンセルします。
        FOR rec IN
            EXECUTE format(
                $f$
                SELECT query_job_id
                    FROM hologres.hg_dynamic_table_refresh_log(%L)
                    WHERE status = 'RUNNING';
                $f$,
                corpus_table
            )
        LOOP
            RAISE NOTICE 'Found running refresh job: %', rec.query_job_id;
            IF hologres.hg_internal_cancel_query_job(rec.query_job_id::bigint) THEN
                RAISE NOTICE 'Cancel job % succeeded.', rec.query_job_id;
            ELSE
                RAISE WARNING 'Cancel job % failed.', rec.query_job_id;
            END IF;
        END LOOP;
    
        -- 2.3 動的テーブルを削除します。
        RAISE NOTICE 'Dropping Dynamic Table: %', corpus_name;
        EXECUTE format('DROP TABLE IF EXISTS %s;', full_corpus_ident);
    
        -- 2.4 オブジェクトテーブルを削除します。
        RAISE NOTICE 'Dropping Object Table: %', obj_table_name;
        EXECUTE format('DROP OBJECT TABLE IF EXISTS %s;', full_obj_ident);
    
        RAISE NOTICE 'Drop complete for corpus: %', corpus_table;
    END;
    $$ LANGUAGE plpgsql;

ベクトル検索関数

-- Q&A のためのベクトル検索
CREATE OR REPLACE FUNCTION qa_vector_search_retrieval(
    question TEXT,
    corpus_table TEXT,
    embedding_model TEXT DEFAULT NULL,
    llm_model TEXT DEFAULT NULL,
    ranking_model TEXT DEFAULT NULL,
    prompt TEXT DEFAULT 'Please answer the following question in ${language} based on the reference information.\n\n Question: ${question}\n\n Reference information:\n\n ${context}',
    language TEXT DEFAULT 'Chinese',
    vector_recall_count INT DEFAULT 20,
    rerank_recall_count INT DEFAULT 5,
    vector_col TEXT DEFAULT 'embedding_vector'
)
RETURNS TEXT AS
$$
DECLARE
    final_answer TEXT;
    sql TEXT;
    embedding_expr TEXT;
    ai_rank_expr TEXT;
    ai_gen_expr TEXT;
    embedding_model_valid BOOLEAN;
    llm_model_valid BOOLEAN;
    ranking_model_valid BOOLEAN;
BEGIN
    embedding_model_valid := (embedding_model IS NOT NULL AND trim(embedding_model) != '');
    llm_model_valid := (llm_model IS NOT NULL AND trim(llm_model) != '');
    ranking_model_valid := (ranking_model IS NOT NULL AND trim(ranking_model) != '');

    IF embedding_model_valid THEN
        embedding_expr := 'ai_embed(' || quote_literal(embedding_model) || ', ' || quote_literal(question) || ')';
    ELSE
        embedding_expr := 'ai_embed(' || quote_literal(question) || ')';
    END IF;

    IF ranking_model_valid THEN
        ai_rank_expr := 'ai_rank(' || quote_literal(ranking_model) || ', ' || quote_literal(question) || ', chunk)';
    ELSE
        ai_rank_expr := 'ai_rank(' || quote_literal(question) || ', chunk)';
    END IF;

    IF llm_model_valid THEN
        ai_gen_expr := 'ai_gen(' || quote_literal(llm_model) ||
            ', replace(replace(replace(' || quote_literal(prompt) ||
               ', ''${question}'', ' || quote_literal(question) || '), ''${context}'', merged_chunks), ''${language}'', ' || quote_literal(language) || ') )';
    ELSE
        ai_gen_expr := 'ai_gen(replace(replace(replace(' || quote_literal(prompt) ||
               ', ''${question}'', ' || quote_literal(question) || '), ''${context}'', merged_chunks), ''${language}'', ' || quote_literal(language) || '))';
    END IF;

    sql := '
      WITH
        embedding_recall AS (
            SELECT
              chunk,
              approx_cosine_distance(' || vector_col || ', ' || embedding_expr || ') AS distance
            FROM
              ' || corpus_table || '
            ORDER BY
              distance DESC
            LIMIT ' || vector_recall_count || '
        ),
        rerank AS (
            SELECT
              chunk,
              ' || ai_rank_expr || ' AS score
            FROM
              embedding_recall
            ORDER BY
              score DESC
            LIMIT ' || rerank_recall_count || '
        ),
        concat_top_chunks AS (
            SELECT string_agg(chunk, E''\n\n----\n\n'') AS merged_chunks FROM rerank
        )
      SELECT ' || ai_gen_expr || '
      FROM concat_top_chunks;
    ';

    EXECUTE sql INTO final_answer;
    RETURN final_answer;
END;
$$ LANGUAGE plpgsql;

全文検索関数

CREATE OR REPLACE FUNCTION qa_text_search_retrieval(
    question TEXT,
    corpus_table TEXT,
    llm_model TEXT DEFAULT NULL,
    ranking_model TEXT DEFAULT NULL,
    prompt TEXT DEFAULT 'Please answer the following question in ${language} based on the reference information.\n\n Question: ${question}\n\n Reference information:\n\n ${context}',
    language TEXT DEFAULT 'Chinese',
    text_search_recall_count INT DEFAULT 20,
    rerank_recall_count INT DEFAULT 5,
    text_search_col TEXT DEFAULT 'chunk'
)
RETURNS TEXT AS
$$
DECLARE
    final_answer TEXT;
    sql TEXT;
    ai_rank_expr TEXT;
    ai_gen_expr TEXT;
    llm_model_valid BOOLEAN;
    ranking_model_valid BOOLEAN;
BEGIN
    llm_model_valid     := (llm_model IS NOT NULL AND trim(llm_model) != '');
    ranking_model_valid := (ranking_model IS NOT NULL AND trim(ranking_model) != '');

    IF ranking_model_valid THEN
        ai_rank_expr := 'ai_rank(' || quote_literal(ranking_model) || ', ' || quote_literal(question) || ', chunk)';
    ELSE
        ai_rank_expr := 'ai_rank(' || quote_literal(question) || ', chunk)';
    END IF;

    IF llm_model_valid THEN
        ai_gen_expr := 'ai_gen(' || quote_literal(llm_model) ||
            ', replace(replace(replace(' || quote_literal(prompt) ||
               ', ''${question}'', ' || quote_literal(question) ||
               '), ''${context}'', merged_chunks), ''${language}'', ' || quote_literal(language) || ') )';
    ELSE
        ai_gen_expr := 'ai_gen(replace(replace(replace(' || quote_literal(prompt) ||
               ', ''${question}'', ' || quote_literal(question) ||
               '), ''${context}'', merged_chunks), ''${language}'', ' || quote_literal(language) || '))';
    END IF;

    sql := '
      WITH
        text_search_recall AS (
            SELECT
              chunk
            FROM
              ' || corpus_table || '
            ORDER BY
              text_search(' || text_search_col || ', ' || quote_literal(question) || ') DESC
            LIMIT ' || text_search_recall_count || '
        ),
        rerank AS (
            SELECT
              chunk,
              ' || ai_rank_expr || ' AS score
            FROM
              text_search_recall
            ORDER BY
              score DESC
            LIMIT ' || rerank_recall_count || '
        ),
        concat_top_chunks AS (
            SELECT string_agg(chunk, E''\n\n----\n\n'') AS merged_chunks FROM rerank
        )
      SELECT ' || ai_gen_expr || '
      FROM concat_top_chunks;
    ';

    EXECUTE sql INTO final_answer;
    RETURN final_answer;
END;
$$ LANGUAGE plpgsql;

ランキング付きハイブリッド検索関数

CREATE OR REPLACE FUNCTION qa_hybrid_retrieval(
    question TEXT,
    corpus_table TEXT,
    embedding_model TEXT DEFAULT NULL,
    llm_model TEXT DEFAULT NULL,
    ranking_model TEXT DEFAULT NULL,
    prompt TEXT DEFAULT 'Please answer the following question in ${language} based on the reference information.\n\n Question: ${question}\n\n Reference information:\n\n ${context}',
    language TEXT DEFAULT 'Chinese',
    text_search_recall_count INT DEFAULT 20,
    vector_recall_count INT DEFAULT 20,
    rerank_recall_count INT DEFAULT 5,
    vector_col TEXT DEFAULT 'embedding_vector',
    text_search_col TEXT DEFAULT 'chunk'
)
RETURNS TEXT AS
$$
DECLARE
    final_answer TEXT;
    sql TEXT;
    embedding_expr TEXT;
    ai_rank_expr TEXT;
    ai_gen_expr TEXT;
    embedding_model_valid BOOLEAN;
    llm_model_valid BOOLEAN;
    ranking_model_valid BOOLEAN;
BEGIN
    embedding_model_valid := (embedding_model IS NOT NULL AND trim(embedding_model) != '');
    llm_model_valid       := (llm_model IS NOT NULL AND trim(llm_model) != '');
    ranking_model_valid   := (ranking_model IS NOT NULL AND trim(ranking_model) != '');

    IF embedding_model_valid THEN
        embedding_expr := 'ai_embed(' || quote_literal(embedding_model) || ', ' || quote_literal(question) || ')';
    ELSE
        embedding_expr := 'ai_embed(' || quote_literal(question) || ')';
    END IF;

    IF ranking_model_valid THEN
        ai_rank_expr := 'ai_rank(' || quote_literal(ranking_model) || ', ' || quote_literal(question) || ', chunk)';
    ELSE
        ai_rank_expr := 'ai_rank(' || quote_literal(question) || ', chunk)';
    END IF;

    IF llm_model_valid THEN
        ai_gen_expr := 'ai_gen(' || quote_literal(llm_model) ||
            ', replace(replace(replace(' || quote_literal(prompt) ||
               ', ''${question}'', ' || quote_literal(question) || '), ''${context}'', merged_chunks), ''${language}'', ' || quote_literal(language) || ') )';
    ELSE
        ai_gen_expr := 'ai_gen(replace(replace(replace(' || quote_literal(prompt) ||
               ', ''${question}'', ' || quote_literal(question) || '), ''${context}'', merged_chunks), ''${language}'', ' || quote_literal(language) || '))';
    END IF;

    sql := '
      WITH
        embedding_recall AS (
            SELECT
              chunk
            FROM
              ' || corpus_table || '
            ORDER BY
              approx_cosine_distance(' || vector_col || ', ' || embedding_expr || ') DESC
            LIMIT ' || vector_recall_count || '
        ),
        text_search_recall AS (
            SELECT
              chunk
            FROM
              ' || corpus_table || '
            ORDER BY
              text_search(' || text_search_col || ', ' || quote_literal(question) || ') DESC
            LIMIT ' || text_search_recall_count || '
        ),
        union_recall AS (
            SELECT chunk FROM embedding_recall
            UNION
            SELECT chunk FROM text_search_recall
        ),
        rerank AS (
            SELECT
              chunk,
              ' || ai_rank_expr || ' AS score
            FROM
              union_recall
            ORDER BY
              score DESC
            LIMIT ' || rerank_recall_count || '
        ),
        concat_top_chunks AS (
            SELECT string_agg(chunk, E''\n\n----\n\n'') AS merged_chunks FROM rerank
        )
      SELECT ' || ai_gen_expr || '
      FROM concat_top_chunks;
    ';

    EXECUTE sql INTO final_answer;
    RETURN final_answer;
END;
$$ LANGUAGE plpgsql;

RRF を使用したハイブリッド検索関数

CREATE OR REPLACE FUNCTION qa_hybrid_retrieval_rrf(
    question TEXT,
    corpus_table TEXT,
    embedding_model TEXT DEFAULT NULL,
    llm_model TEXT DEFAULT NULL,
    ranking_model TEXT DEFAULT NULL,
    prompt TEXT DEFAULT 'Please answer the following question in ${language} based on the reference information.\n\n Question: ${question}\n\n Reference information:\n\n ${context}',
    language TEXT DEFAULT 'Chinese',
    text_search_recall_count INT DEFAULT 20,
    vector_recall_count INT DEFAULT 20,
    rerank_recall_count INT DEFAULT 5,
    rrf_k INT DEFAULT 60,
    vector_col TEXT DEFAULT 'embedding_vector',
    text_search_col TEXT DEFAULT 'chunk'
)
RETURNS TEXT AS
$$
DECLARE
    final_answer TEXT;
    sql TEXT;
    embedding_expr TEXT;
    ai_rank_expr TEXT;
    ai_gen_expr TEXT;
    embedding_model_valid BOOLEAN;
    llm_model_valid BOOLEAN;
    ranking_model_valid BOOLEAN;
BEGIN
    embedding_model_valid := (embedding_model IS NOT NULL AND trim(embedding_model) <> '');
    llm_model_valid       := (llm_model IS NOT NULL AND trim(llm_model) <> '');
    ranking_model_valid   := (ranking_model IS NOT NULL AND trim(ranking_model) <> '');

    IF embedding_model_valid THEN
        embedding_expr := 'ai_embed(' || quote_literal(embedding_model) || ', ' || quote_literal(question) || ')';
    ELSE
        embedding_expr := 'ai_embed(' || quote_literal(question) || ')';
    END IF;

    IF ranking_model_valid THEN
        ai_rank_expr := 'ai_rank(' || quote_literal(ranking_model) || ', ' || quote_literal(question) || ', chunk)';
    ELSE
        ai_rank_expr := 'ai_rank(' || quote_literal(question) || ', chunk)';
    END IF;

    IF llm_model_valid THEN
        ai_gen_expr := 'ai_gen(' || quote_literal(llm_model) ||
            ', replace(replace(replace(' || quote_literal(prompt) ||
                ', ''${question}'', ' || quote_literal(question) || '), ''${context}'', merged_chunks), ''${language}'', ' || quote_literal(language) || ') )';
    ELSE
        ai_gen_expr := 'ai_gen(replace(replace(replace(' || quote_literal(prompt) ||
                ', ''${question}'', ' || quote_literal(question) || '), ''${context}'', merged_chunks), ''${language}'', ' || quote_literal(language) || '))';
    END IF;

    sql := '
      WITH embedding_recall AS (
        SELECT
          chunk,
          vec_score,
          ROW_NUMBER() OVER (ORDER BY vec_score DESC) AS rank_vec
        FROM (
          SELECT
            chunk,
            approx_cosine_distance(' || vector_col || ', ' || embedding_expr || ') AS vec_score
          FROM
            ' || corpus_table || '
        ) t
        ORDER BY vec_score DESC
        LIMIT ' || vector_recall_count || '
      ),
      text_search_recall AS (
        SELECT
          chunk,
          text_score,
          ROW_NUMBER() OVER (ORDER BY text_score DESC) AS rank_text
        FROM (
          SELECT
            chunk,
            text_search(' || text_search_col || ', ' || quote_literal(question) || ') AS text_score
          FROM
            ' || corpus_table || '
        ) ts
        WHERE text_score > 0
        ORDER BY text_score DESC
        LIMIT ' || text_search_recall_count || '
      ),
      rrf_scores AS (
        SELECT
          chunk,
          SUM(1.0 / (' || rrf_k || ' + rank_val)) AS rrf_score
        FROM (
          SELECT chunk, rank_vec AS rank_val FROM embedding_recall
          UNION ALL
          SELECT chunk, rank_text AS rank_val FROM text_search_recall
        ) sub
        GROUP BY chunk
      ),
      top_chunks AS (
        SELECT chunk
        FROM rrf_scores
        ORDER BY rrf_score DESC
        LIMIT ' || rerank_recall_count || '
      ),
      concat_top_chunks AS (
        SELECT string_agg(chunk, E''\n\n----\n\n'') AS merged_chunks FROM top_chunks
      )
      SELECT ' || ai_gen_expr || '
      FROM concat_top_chunks;
    ';

    EXECUTE sql INTO final_answer;
    RETURN final_answer;
END;
$$ LANGUAGE plpgsql;