Hybrid search
=============

ハイブリッド検索は、ナレッジベースの埋め込みのセマンティックベクター/埋め込み検索と1つ以上の追加フィルターメカニズムを組み合わせます。

- **リレーショナル述語フィルタリング** —
  ソーステーブルの任意の列、たとえば ``WHERE category = 'vegetarian'``
  や\ ``WHERE published_date > '2024-01-01'``
  などで結果をフィルタリングします。

- **BM25全文検索**
  BM25インデックスを使用してキーワードの関連性によって結果をランク付けし、特定の単語またはフレーズが結果に表示されるようにします。

これら2つのアプローチは、セマンティック検索と並行して、独立してまたは一緒に使用できます。

AIパイプラインは、埋め込み生成を含むナレッジベースの構築を通常のように処理します。ハイブリッド検索は、クエリー時に、ベクターテーブルとソーステーブルを組み合わせたカスタムSQLを構成することにより適用されます。

前提条件
--------

pgvector反復インデックススキャン
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

pgvector 0.8.0以降が必要ですEDB Postgres AIのデフォルトは0.8.1です。

反復インデックススキャンは、ベクトル\ ``ORDER BY`` と\ ``WHERE``
フィルターを効率的に組み合わせるためのキーイネーブラーです。これらがないと、ベクトルインデックスはフィルタリング前に固定数の候補をプリフェッチします。つまり、選択性の高いフィルタが\ ``LIMIT``
要求よりも少ない結果を返す場合があります。反復スキャンが有効になっている場合、インデックスはフィルタリング後でも\ ``LIMIT``
が満足するまで候補を生成し続けます。

HNSWインデックスの反復スキャンを有効にします。

.. code:: sql

   SET hnsw.iterative_scan = relaxed_order;

IVFFlatインデックスの場合

.. code:: sql

   SET ivfflat.iterative_scan = on;

非常に選択的なフィルター一致する行がほとんどない場合、安全制限を設定して、スキャンがインデックス全体を走査しないようにします。

.. code:: sql

   SET hnsw.iterative_scan_max_tuples = 20000;

..  Important::
   反復スキャンを有効にするには、ベクトル`ORDER BY` と`WHERE` フィルターが同じクエリレベルである必要がありますCTE境界で分離されていません。以下のクエリパターンは、それに応じて構造化されています。

BM25オプション
^^^^^^^^^^^^^^

BM25フルテキストフィルタリングには、 ``vchord_bm25``
および :ref:`pg_tokenizer </pg_extensions/vectorchord_bm25/>` 拡張機能が必要です。これらはユーザーがインストールおよび管理します。設定の詳細については、
:ref:`VectorChord-BM25 <VectorChord-BM25>` を参照してください。

ヘルパーファンクションとメタデータ
----------------------------------

promoteb.kb_query_encode()
^^^^^^^^^^^^^^^^^^^^^^^^^^

モデル名を検索する必要なく、特定のナレッジベース用に構成された埋め込みモデルを使用してテキストクエリーをエンコードします。

.. code:: sql

   SELECT aidb.kb_query_encode(public.my_kb, test query);

カスタムベクタークエリを構築するときに、
``aidb.encode_text('<model>', '<query>')`` の代わりにこれを使用します。

``aidb.knowledge_bases`` ビュー
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``aidb.knowledge_bases`` ビュー ``aidb.kbs``
としてもアクセス可能は、ハイブリッドクエリーの構成に必要なすべてのメタデータを公開します。

.. csv-table::
  :header: Column,Type,Description
  :widths: 12,15,25
  :align: left
  :class: longtable

  `id`,integer,内部識別子
  `name`,text,ナレッジベース名
  `vector_schema`,text,エンベディングテーブルのスキーマ
  `vector_table`,text,エンベディングストレージテーブル
  `model_name`,text,このKBで使用される埋め込みモデル
  `distance_operator`,aidb.distanceoperator,距離メトリックの列挙値
  `distance_operator_sql`,text,SQL演算子構文の距離演算子
  `vector_data_column`,text,エンベディングを格納する列
  `vector_key_column`,text,埋め込みをソース行にリンクする列
  `vector_index`,jsonb,ベクトルインデックス構成
  `pipeline_ids`,integer[],このナレッジベースに接続されているすべてのパイプラインのID
  `pipeline_names`,text[],このナレッジベースに接続されているすべてのパイプラインの名前

.. code:: sql

   SELECT name, vector_schema, vector_table, distance_operator_sql
   FROM aidb.knowledge_bases;

promoteb.rerank_text()
^^^^^^^^^^^^^^^^^^^^^^

**オプショナル**
候補者を取得した後、最終的な関連性の高い結果を得るためのリランキングモデルに渡します。

.. code:: sql

   SELECT aidb.rerank_text(my_reranking_model, query text, ARRAY[result1, result2, ...]);

``nim_reranking`` や\ ``hf_tei_reranking``
などの再ランキングモデルは、取得後の別の後処理ステップとしてサポートされています。

--------------

クエリパターン
--------------

ベクトル+関係フィルター
^^^^^^^^^^^^^^^^^^^^^^^

ソーステーブルの任意の列でセマンティック検索結果をフィルタリングします。フィルタはベクトル\ ``ORDER BY``
と同じクエリレベルで適用されるため、反復インデックススキャンで正確に\ ``LIMIT``
の結果を生成できます。

.. code:: sql

   SET hnsw.iterative_scan = relaxed_order;

   SELECT s.id, s.description,
          kb.value <=> aidb.kb_query_encode(public.my_kb_vector, summer fashion) AS distance
   FROM my_kb_vector kb
   JOIN my_source_table s ON s.id::TEXT = kb.source_id
   WHERE s.category_id = 2                          -- relational filter on source table
   ORDER BY kb.value <=> aidb.kb_query_encode(public.my_kb_vector, summer fashion)
   LIMIT 10;

Vector + BM25フィルタとして
^^^^^^^^^^^^^^^^^^^^^^^^^^^

``WHERE``
条件としてBM25スコアしきい値を使用し、結果をベクトル距離で並べ替えます。これにより、キーワード一致も満たす、意味的に最も近い結果が返されます。

.. code:: sql

   SET hnsw.iterative_scan = relaxed_order;

   SELECT s.id, s.description,
          kb.value <=> aidb.kb_query_encode(public.my_kb_vector, summer fashion)  AS distance
   FROM my_kb_vector kb
   JOIN my_source_table s ON s.id::TEXT = kb.source_id
   WHERE s.bm25_col <&> to_bm25query(my_bm25_idx, tokenize(shoes, bert)) < -1.0  -- BM25 filter
   ORDER BY kb.value <=> aidb.kb_query_encode(public.my_kb_vector, summer fashion) 
   LIMIT 10;

Vector + BM25相互ランク融合経由
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Reciprocal Rank
FusionRRFは、セマンティック検索とキーワード検索を独立して実行し、結果セットをマージおよびスコア付けします。これにより、オーバーフィルタリングが回避され、いずれのディメンションでも良好なランクの結果が表面化されます。

.. code:: sql

   WITH semantic AS (
       SELECT source_id AS id,
              ROW_NUMBER() OVER (ORDER BY value <=> aidb.kb_query_encode(public.my_kb_vector, summer fashion) AS rank
       FROM my_kb_vector
       ORDER BY value <=> aidb.kb_query_encode(public.my_kb_vector, summer fashion) 
       LIMIT 50
   ),
   fulltext AS (
       SELECT id::TEXT AS id,
              ROW_NUMBER() OVER (ORDER BY bm25_col <&> to_bm25query(my_bm25_idx, tokenize(shoes, bert))) AS rank
       FROM my_source_table
       ORDER BY bm25_col <&> to_bm25query(my_bm25_idx, tokenize(shoes, bert))
       LIMIT 50
   )
   SELECT COALESCE(s.id, f.id) AS id,
          COALESCE(1.0 / (60 + s.rank), 0) + COALESCE(1.0 / (60 + f.rank), 0) AS rrf_score
   FROM semantic s
   FULL OUTER JOIN fulltext f ON s.id = f.id
   ORDER BY rrf_score DESC
   LIMIT 10;

RRF式の定数\ ``60``
は、標準のランク平滑化係数です。これを調整して、高ランクと低ランクの結果の相対的な重みを制御します。

Vector +リレーショナル+BM25すべて3つ
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

3つのフィルタリングメカニズムすべてを単一のクエリーに組み合わせます。

.. code:: sql

   SET hnsw.iterative_scan = relaxed_order;

   SELECT s.id, s.description,
          kb.value <=> aidb.kb_query_encode(public.my_kb_vector, summer fashion)  AS distance,
          s.bm25_col <&> to_bm25query(my_bm25_idx, tokenize(shoes, bert)) AS bm25_score
   FROM my_kb_vector kb
   JOIN my_source_table s ON s.id::TEXT = kb.source_id
   WHERE s.category_id = 2                                                                    -- relational filter
     AND s.bm25_col <&> to_bm25query(my_bm25_idx, tokenize(shoes, bert)) < -1.0        -- BM25 filter
   ORDER BY kb.value <=> aidb.kb_query_encode(public.my_kb_vector, summer fashion) 
   LIMIT 10;

--------------

実用的な例
----------

例1リレーショナル述語フィルタリング
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

この例は、セマンティックベクトル検索とリレーショナル\ ``WHERE``
フィルターを組み合わせて、結果を特定のカテゴリに絞り込む方法を示しています。パイプラインは製品名をナレッジベースに埋め込み、クエリによってカテゴリでフィルターされた意味的に同様の結果を取得します。

**ソーステーブルとパイプラインの設定**

ソーステーブルを作成します。

.. code:: sql

   CREATE TABLE products (
       id           SERIAL PRIMARY KEY,
       product_name TEXT NOT NULL,
       category     TEXT NOT NULL
   );

サンプルデータを入力します。

.. code:: sql

   INSERT INTO products (product_name, category) VALUES
       (Hamburger,    meat),
       (Cheeseburger, meat),
       (Fish n Chips,  fish),
       (Fries,         vegetarian),
       (Burrito,       meat),
       (Pizza,         meat),
       (Sandwich,      meat),
       (Veggie Burger, vegetarian),
       (Kebab,         meat);

``product_name`` をナレッジベースに埋め込むパイプラインを作成します。

.. code:: sql

   SELECT aidb.create_pipeline(
       name               => food_products,
       source             => products,
       source_key_column  => id,
       source_data_column => product_name,
       step_1             => KnowledgeBase,
       step_1_options     => aidb.knowledge_base_config(
                                 model       => bert,
                                 data_format => Text
                             )
   );

パイプラインを実行してエンベディングを生成します。

.. code:: sql

   SELECT aidb.run_pipeline(food_products);

**標準のセマンティッククエリーフィルタなし**

.. code:: sql

   SELECT * FROM aidb.retrieve_text(public.pipeline_food_products, fast food, topk => 4);
   __OUTPUT__
    key |     value     |      distance      | part_ids | intermediate_steps
   - ----+---------------+--------------------+----------+--------------------
    8   | Veggie Burger | 0.9955745765192575 | {0}      | []
    6   | Pizza         | 1.0280680840689795 | {0}      | []
    4   | Fries         | 1.0630385212203115 | {0}      | []
    7   | Sandwich      | 1.0903594734917037 | {0}      | []

**リレーショナルフィルターを使用したカスタムクエリー**

.. code:: sql

   SET hnsw.iterative_scan = relaxed_order;

   SELECT vector.source_id, source.product_name, source.category
   FROM public.pipeline_food_products AS vector
   JOIN products AS source ON source.id::TEXT = vector.source_id
   WHERE source.category = vegetarian                                                         -- relational filter
   ORDER BY vector.value <=> aidb.kb_query_encode(public.pipeline_food_products, fast food) 
   LIMIT 10;
   __OUTPUT__
    source_id | product_name  |  category
   - ----------+---------------+------------
    8         | Veggie Burger | vegetarian
    4         | Fries         | vegetarian

ベジタリアン商品のみが返され、「ファーストフード」との意味的な関連性によって順序付けられます。

--------------

例2 RRFを使用したBM25全文検索
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

この例では、逆位ランク融合RRFを使用してセマンティックベクトル検索とBM25キーワード検索を組み合わせて、2つの結果セットをマージする方法を示しています。
RRFは、セマンティックランキングとキーワードランキングでの位置を組み合わせて結果を再ランク付けし、両方で高いスコアの項目を表示します。この例は、例1と同じ\ ``products``
テーブルと\ ``food_products`` パイプラインに基づいて構築されています。

**ソーステーブルにBM25インデックスを追加します**

``pg_tokenizer`` および\ ``vchord_bm25`` 拡張機能を有効にします。

.. code:: sql

   CREATE EXTENSION IF NOT EXISTS pg_tokenizer;
   CREATE EXTENSION IF NOT EXISTS vchord_bm25;

BERTモデルを使用してトークナイザーを作成します。

.. code:: sql

   SELECT tokenizer_catalog.create_tokenizer(bert, $$
       model = "bert_base_uncased"
   $$);

BM25ベクトル列をソーステーブルに追加し、トークン化された値を入力します。

.. code:: sql

   ALTER TABLE products ADD COLUMN embedding_bm25 bm25_catalog.bm25vector;
   UPDATE products SET embedding_bm25 = tokenizer_catalog.tokenize(product_name, bert);

新しい列にBM25インデックスを作成します。

.. code:: sql

   CREATE INDEX products_bm25_idx ON products USING bm25 (embedding_bm25 bm25_catalog.bm25_ops);

**RRFを使用したハイブリッドクエリ**

.. code:: sql

   SET hnsw.iterative_scan = relaxed_order;
   SET search_path TO public, tokenizer_catalog, bm25_catalog;

   WITH semantic AS (
       SELECT source.id,
              source.product_name,
              source.category,
              ROW_NUMBER() OVER (ORDER BY vector.value <=> aidb.kb_query_encode(public.pipeline_food_products, fast food) AS rank
       FROM public.pipeline_food_products AS vector
       JOIN products AS source ON source.id::TEXT = vector.source_id
       ORDER BY vector.value <=> aidb.kb_query_encode(public.pipeline_food_products, fast food)
       LIMIT 20
   ),
   keyword AS (
       SELECT id,
              product_name,
              ROW_NUMBER() OVER (ORDER BY embedding_bm25 <&> to_bm25query(products_bm25_idx, tokenize(fast food, bert))) AS rank
       FROM products
       ORDER BY embedding_bm25 <&> to_bm25query(products_bm25_idx, tokenize(fast food, bert))
       LIMIT 20
   )
   SELECT
       COALESCE(s.id, k.id)                                                         AS id,
       COALESCE(s.product_name, k.product_name)                                     AS product_name,
       s.category,
       s.rank                                                                       AS semantic_rank,
       k.rank                                                                       AS keyword_rank,
       COALESCE(1.0 / (60 + s.rank), 0) + COALESCE(1.0 / (60 + k.rank), 0)        AS rrf_score
   FROM semantic s
   FULL OUTER JOIN keyword k ON s.id = k.id
   ORDER BY rrf_score DESC
   LIMIT 10;
   __OUPUT__
    id | product_name  |  category  | semantic_rank | keyword_rank |       rrf_score
   - ---+---------------+------------+---------------+--------------+------------------------
     1 | Hamburger     | meat       |             6 |            1 | 0.03154495777446597119
     4 | Fries         | vegetarian |             3 |            4 | 0.03149801587301587302
     6 | Pizza         | meat       |             2 |            6 | 0.03128054740957966765
     8 | Veggie Burger | vegetarian |             1 |            8 | 0.03109932497589199614
     3 | Fish n Chips  | fish       |             7 |            3 | 0.03079838900734423123
     5 | Burrito       | meat       |             5 |            5 | 0.03076923076923076924
     2 | Cheeseburger  | meat       |             9 |            2 | 0.03062178588125292193
     7 | Sandwich      | meat       |             4 |            7 | 0.03055037313432835821
     9 | Kebab         | meat       |             8 |            9 | 0.02919863597612958227

RRFスコアは、両方のランキングを組み合わせたものです。セマンティックまたはキーワード検索サーフェスのいずれかで高ランクのアイテムが上部にあり、両方でよくランクされているアイテムが最高スコアを取得します。

--------------

ユーザー管理のBM25のセットアップ
--------------------------------

BM25は、AIパイプラインによって管理されません。ソーステーブルで独立して設定し、ハイブリッドクエリで参照します。

**1.拡張機能のインストール**

.. code:: sql

   CREATE EXTENSION IF NOT EXISTS pg_tokenizer;
   CREATE EXTENSION IF NOT EXISTS vchord_bm25;

**2.トークナイザーを作成します**

.. code:: sql

   SELECT create_tokenizer(bert, $$
       model = "bert_base_uncased"
   $$);

**3. ``bm25vector`` 列とインデックスをソーステーブルに追加します**

.. code:: sql

   ALTER TABLE my_source ADD COLUMN bm25_tokens bm25vector;
   UPDATE my_source SET bm25_tokens = tokenize(description, bert);

   CREATE INDEX my_source_bm25 ON my_source USING bm25 (bm25_tokens bm25_ops);

**4.インデックスを最新の状態に保ちます**

新しい行は、BM25結果に表示される前にトークン化する必要があります。データ取り込みプロセスの一部としてトリガーを使用するか、列を更新します。

.. code:: sql

   UPDATE my_source SET bm25_tokens = tokenize(description, bert) WHERE bm25_tokens IS NULL;

トークナイザーの完全な構成オプションについては、
:ref:`VectorChord-BM25 <VectorChord-BM25>` および :ref:`VectorChord-BM25 in pg_extensions </pg_extensions/vectorchord_bm25/>` を参照してください。

--------------

ボリュームソースの制限
----------------------

リレーショナル述語フィルタリングまたはBM25を使用したハイブリッド検索は、
**ボリュームソースのナレッジベースではサポートされていません**
。ボリュームソースには、結合するバッキングPostgresテーブルがないため、フィルタまたはインデックスを作成するソーステーブルがありません。

ボリュームソースのナレッジベースは、 ``aidb.retrieve_text()``
および\ ``aidb.retrieve_key()``
を介して標準のセマンティック検索を引き続きサポートします。
