全部產品
Search
文件中心

ApsaraDB RDS:使用RDS PostgreSQL打造RAG應用

更新時間:Feb 25, 2025

RDS PostgreSQL具備強大的向量儲存與高效的全文檢索索引能力,使其成為檢索增強產生(RAG)應用的理想向量資料庫,能夠輕鬆實現基於關鍵詞的檢索。

文檔導讀

本文以基於RDS PostgreSQL構建專屬工單機器人為例,介紹RDS PostgreSQL在構建RAG應用方面的優勢。您將瞭解到:

  • 基於RDS PostgreSQL構建專屬工單機器人的構建流程,以及在各個環節中RDS PostgreSQL相關外掛程式及函數的使用方法。

  • 通過簡單的多路召回使用樣本,體驗RDS PostgreSQL在RAG中的強大功能和易用性。

構建流程

基於RDS PostgreSQL構建專屬工單機器人,整體流程可劃分為:資料處理、多路召回、融合排序及問答分析。

  1. 資料處理

    將來源資料(例如官方文檔、知識庫、歷史工單等)進行處理,包括切分和向量化(embedding),並將其儲存至RDS PostgreSQL資料庫中。

  2. 多路召回

    • 文檔關鍵詞召回:對問題與文檔表中的關鍵詞進行相似性匹配,返回匹配度最高的前N篇文檔。

    • 內容關鍵詞召回:基於文檔內容的關鍵詞進行檢索,對使用者問題與文檔內容的關鍵詞進行相似性匹配。

    • BM25召回:基於詞頻、詞與詞之間的距離,以及它們所歸屬文檔模組的重要性,對相關性評分進行統計分析,返回評分靠前的內容。

    • 向量召回:將問題轉換為向量形式,並計算其餘弦相似性,返回相似性靠前的內容。

  3. 融合排序

    對多路召回的結果,採用倒數排序融合(Reciprocal Rank Fusion, RRF)演算法及開源的bce-reranker-base_v1模型進行精確排序。

  4. 問答分析

    將問題及其答案儲存於資料庫中,在測試階段對問答效果進行評分,以評估不同檢索策略的效果。

資料處理

資料處理是指對來源資料(例如官方文檔、知識庫、歷史工單等)進行處理,包括切分和向量化(embedding),並將其儲存至RDS PostgreSQL資料庫中。

擷取資料

請根據RAG應用的目的擷取相關資料。本文以RDS PostgreSQLDingTalk工單機器人為例,收集了相應的協助文檔、知識庫及歷史工單。

處理資料

對來源資料進行處理,按照特定格式儲存到RDS PostgreSQL資料庫中。例如,針對協助文檔,使用LangChain架構提供的HTMLHeaderTextSplitter類,將文本按照HTML的層級(如H1、H2等)進行切分。您可以指定切分的大小和重疊大小,從而靈活控制文本的切分效果。具體請參見LangChain提供的各種Text splitters的切割方法。

說明

如果處理的是Markdown文檔,則可以利用MarkdownHeaderTextSplitter類,通過#、##等標記進行層級分割。

儲存資料

資料存放區的核心表主要包括兩個:一個是用於儲存文檔資訊的文檔表(document),另一個是用於儲存切分後資訊的向量表(embedding)。

  • document表

    例如:

    \d document
                                               資料表 "public.document"
           欄位        |            類型             | 校對規則 |  可空的  |                 預設
    -------------------+-----------------------------+----------+----------+--------------------------------------
     id                | bigint                      |          | not null | nextval('document_id_seq'::regclass)
     title             | character varying(255)      |          |          |
     url               | character varying(255)      |          |          | ''::character varying
     key_word          | character varying(255)      |          |          | ''::character varying
     tag               | character varying(255)      |          |          | ''::character varying
     created           | timestamp without time zone |          | not null | now()
     modified          | timestamp without time zone |          | not null | now()
     key_word_tsvector | tsvector                    |          |          |
     product_name      | character varying(255)      |          |          | ''::character varying
    索引:
        "document_pkey" PRIMARY KEY, btree (id)
        "document_key_word_tsvector_gin" gin (key_word_tsvector)
        "document_product_name_key" btree (product_name)
        "document_title_key" UNIQUE CONSTRAINT, btree (title)
    觸發器:
        trigger_update_tsvector BEFORE INSERT OR UPDATE ON document FOR EACH ROW EXECUTE FUNCTION update_tsvector()
    • document表

      包含以下欄位:ID、標題(title)、URL連結(url)、關鍵詞(key_word)、標籤(tag)、建立時間(created)、修改時間(modified)、將關鍵詞轉換為加權的tsvector類型(key_word_tsvector,以便進行關鍵詞匹配,作為內容召回的一部分),以及產品名稱(product_name)。

    • 索引

      包括:主鍵ID索引、文檔標題的唯一索引(目前不允許文檔名重複,文檔更新時依據文檔名進行更新)、產品名稱索引,以及將關鍵詞轉換為tsvector類型後建立的倒排索引(在RDS PostgreSQL中可使用GIN索引)。

    • 觸發器

      目的是在更新和插入document表時,自動更新key_word_tsvector列的內容。

      建立觸發器的SQL語句及其簡要解釋

      1. 首先建立一個函數,用於在插入或更新表時檢查標籤(tag)列的內容。當tag為direct時,將關鍵詞(key_word)轉換為tsvector類型,並賦予A權重。

        說明

        RDS PostgreSQL的setweight函數可以設定tsvector的權重,從高到低分別為A、B、C和D。此權重設定的目的是在關鍵詞召回時優先考慮tag為direct的文檔,其次是來自工單(aone)的文檔,按照優先順序依次設定。

      2. 建立觸發器自動執行update_tsvector()函數,以便在document表更新時更新key_word_tsvector列的內容。

      CREATE OR REPLACE FUNCTION update_tsvector()
      RETURNS TRIGGER AS $$
      BEGIN
          IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
              IF NEW.tag = 'direct' THEN
                  NEW.key_word_tsvector := setweight(to_tsvector('jiebacfg', NEW.key_word), 'A');
              ELSIF NEW.tag = 'aone' THEN
                  NEW.key_word_tsvector := setweight(to_tsvector('jiebacfg', NEW.key_word), 'B');
              ELSIF NEW.tag IS NOT NULL THEN
                  NEW.key_word_tsvector := setweight(to_tsvector('jiebacfg', NEW.key_word), 'C');
              ELSE
                  NEW.key_word_tsvector := setweight(to_tsvector('jiebacfg', NEW.key_word), 'D');
              END IF;
          END IF;
      
          RETURN NEW;
      END;
      $$ LANGUAGE plpgsql;
      CREATE FUNCTION
        
      CREATE TRIGGER trigger_update_tsvector
      BEFORE INSERT OR UPDATE ON document
      FOR EACH ROW
      EXECUTE FUNCTION update_tsvector();
      CREATE TRIGGER
  • embedding表

    例如:

    \d embedding
                                               資料表 "public.embedding"
           欄位        |            類型             | 校對規則 |  可空的  |                 預設
    -------------------+-----------------------------+----------+----------+---------------------------------------
     id                | bigint                      |          | not null | nextval('embedding_id_seq'::regclass)
     doc_id            | integer                     |          |          | '-1'::integer
     content_chunk     | text                        |          | not null |
     content_embedding | vector(1536)                |          | not null |
     created           | timestamp without time zone |          | not null | now()
     modified          | timestamp without time zone |          | not null | now()
     ts_vector_extra   | tsvector                    |          |          |
    索引:
        "embedding_pkey" PRIMARY KEY, btree (id)
        "embedding_content_embedding_idx" hnsw (content_embedding vector_cosine_ops) WITH (m='16', ef_construction='64')
        "embedding_doc_id_key" btree (doc_id)
        "embedding_rumidx" rum (ts_vector_extra)
    觸發器:
        embedding_tsvector_update BEFORE INSERT OR UPDATE ON embedding FOR EACH ROW EXECUTE FUNCTION tsvector_update_trigger('ts_vector_extra', 'public.jiebacfg', 'content_chunk')
    • embedding表

      包括以下欄位:ID、文檔ID(doc_id)、切分後的文檔內容(content_chunk)、文檔內容的向量化表示(content_embedding)、建立時間(created)、修改時間(modified),以及將切分後的文檔轉換為tsvector類型(ts_vector_extra)。

    • 索引

      包括:主鍵索引、關聯的文檔ID索引、對embedding的向量類型列建立向量索引,以及為tsvector類型列建立RUM索引。

    • 觸發器

      目的是在更新和插入embedding表時,自動更新ts_vector_extra列的內容。

      建立觸發器的SQL語句及其簡要解釋

      其建立SQL與document表的觸發器相似。

      CREATE TRIGGER embedding_tsvector_update
      BEFORE INSERT OR UPDATE ON embedding
      FOR EACH ROW
      EXECUTE PROCEDURE tsvector_update_trigger('ts_vector_extra','public.jiebacfg','content_chunk');

多路召回

文檔關鍵詞召回

對使用者問題與文檔表中的關鍵詞進行相似性匹配,返回匹配度最高的前N篇文檔。

文檔關鍵詞召回的核心在於如何高效地對RDS PostgreSQL資料庫中的關鍵詞與使用者提出的問題進行相似性匹配。本方案採用RDS PostgreSQL內建的GIN索引(通用倒排索引,Generalized Inverted Index)來實現此目標。

  1. 將文檔中的關鍵詞轉換為tsvector類型,並對其進行加權處理。

    在轉換過程中指定分詞類型。常用的中文分詞外掛程式包括中文分詞(pg_jieba)中文分詞(zhparser)。有關外掛程式的安裝,請參見管理外掛程式

    通過使用to_tsvector函數,可以對關鍵詞進行切分;同時,利用內建函數setweight,對切分後的字串進行加權處理,以在其位置資訊中增加加權資訊。例如,使用外掛程式pg_jieba,將分詞類型指定為jiebacfg,並使用setweight對文檔關鍵詞設定加權為A。

    SELECT setweight(to_tsvector('jiebacfg', 'PostgreSQL是世界上先進的開源關係型資料庫'), 'A');
                                       setweight
    -------------------------------------------------------------------------------
     'postgresql':1A '世界':3A '先進':5A '關係':8A '型':9A '開源':7A '資料庫':10A
  2. 將使用者的問題轉換為tsquery查詢類型,並實現對文檔關鍵字的相似性匹配。

    利用RDS PostgreSQL的全文檢索索引能力,實現問題查詢與文檔關鍵字的相似性匹配,相關SQL語句如下所示。

    SELECT 
        id, 
        title, 
        url, 
        key_word, 
        ts_rank(
            key_word_tsvector, 
            to_tsquery(replace(text(plainto_tsquery('jiebacfg', '%s')), '&', '|'))
        ) AS score 
    FROM 
        public.document 
    WHERE 
        key_word_tsvector @@ to_tsquery(replace(text(plainto_tsquery('jiebacfg', '%s')), '&', '|')) 
        AND product_name = '%s' 
    ORDER BY 
        score DESC 
    LIMIT 1;
    • to_tsquery函數

      使用to_tsquery函數,將使用者問題轉換成tsquery查詢類型。例如:

      SELECT to_tsquery('jiebacfg', 'PostgreSQL是世界上先進的開源關係型資料庫');
                                         to_tsquery
      --------------------------------------------------------------------------------
       'postgresql' <2> '世界' <2> '先進' <2> '開源' <-> '關係' <-> '型' <-> '資料庫'

      <2>表示前後兩個詞彙之間的間距,而<->則表示相鄰的關係,例如開源關係需相鄰出現。此外,常見的停用詞(例如,等)將被自動移除。除此之外,還有其他常用符號:&表示並且,|表示或者,!表示非,同時也可以加權表示,例如:

      SELECT to_tsquery('jiebacfg', 'postgresql:A');
         to_tsquery
      ----------------
       'postgresql':A

      to_tsquery函數的輸入必須符合tsquery的操作符要求。為了確保在將輸入的問題轉換為to_tsquery時不發生異常,使用plainto_tsquery函數能夠有效地忽略非法的操作符。例如:

      SELECT to_tsquery('jiebacfg','日誌|&堆積');
      ERROR:  syntax error in tsquery: "日誌|&堆積"
        
      SELECT plainto_tsquery('jiebacfg','日誌|&堆積');
       plainto_tsquery
      -----------------
       '日誌' & '堆積'

      使用text函數將plainto_tsquery返回的結果轉換為文字格式設定,並使用replace函數將其中的&替換為|,實現匹配條件從“並且”轉換為“或者”。不同函數的處理結果如下所示:

      --使用plainto_tsquery函數
      SELECT plainto_tsquery('jiebacfg', 'PostgreSQL是世界上先進的開源關係型資料庫');
                                plainto_tsquery
      --------------------------------------------------------------------
       'postgresql' & '世界' & '先進' & '開源' & '關係' & '型' & '資料庫'
      
      --使用plainto_tsquery、text和replace函數
      SELECT replace(text(plainto_tsquery('jiebacfg', 'PostgreSQL是世界上先進的開源關係型資料庫')), '&', '|');
                                    replace
      --------------------------------------------------------------------
       'postgresql' | '世界' | '先進' | '開源' | '關係' | '型' | '資料庫'
      
      --使用to_tsquery、plainto_tsquery、text和replace函數
      SELECT to_tsquery(replace(text(plainto_tsquery('jiebacfg', 'PostgreSQL是世界上先進的開源關係型資料庫')), '&', '|'));
                                   to_tsquery
      --------------------------------------------------------------------
       'postgresql' | '世界' | '先進' | '開源' | '關係' | '型' | '資料庫'

      使用pg_jieba外掛程式的自訂分詞功能,自訂分詞方式。例如,將分詞關係型添加至自訂字典中,添加前的分詞結果為'關係' & '型',添加後的分詞結果為'關係型'

      --預設將分詞“關係型”添加到0號詞典中,權重為100000
      INSERT INTO jieba_user_dict VALUES ('關係型',0,100000);
      
      --載入0號詞典;第一個0代表自訂字典序號,第二個0代表載入預設詞典。
      SELECT jieba_load_user_dict(0,0);
       jieba_load_user_dict
      ----------------------
      
      --將使用者問題轉換成tsquery查詢類型
      SELECT to_tsquery(replace(text(plainto_tsquery('jiebacfg', 'PostgreSQL是世界上先進的開源關係型資料庫')), '&', '|'));
                                   to_tsquery
      --------------------------------------------------------------------
       'postgresql' | '世界' | '先進' | '開源' | '關係型' | '資料庫' 
    • 運算子

      RDS PostgreSQL提供多種運算子,對查詢類型tsquery和經過分詞處理的類型tsvector進行運算。例如,@@用於表示tsvector是否與tsquery匹配。

      SELECT to_tsvector('jiebacfg', 'PostgreSQL是世界上先進的開源關係型資料庫') @@ to_tsquery('jiebacfg', 'postgresql:A');
       ?column?
      ----------
       f
      
      
      SELECT setweight(to_tsvector('jiebacfg', 'PostgreSQL是世界上先進的開源關係型資料庫'),'A') @@ to_tsquery('jiebacfg', 'postgresql:A');
       ?column?
      ----------
       t

      查詢條件為加權A的單詞postgresql,在此情況下,即使待查詢的tsvector中包含postgresql一詞,由於未賦予加權A,查詢結果將返回false;相反,如果待查詢的tsvector中使用setweight函數設定了加權為A,則查詢結果將返回true。

    • ts_rank函數

      使用ts_rank函數,計算查詢類型tsquery與待查詢類型tsvector之間的匹配程度。例如,查詢條件使用postgresql | 開源進行匹配計算。即待查詢行需包含postgresql或者開源其中一個關鍵詞。第一個句子同時包含了這兩個詞,因此其得分高於僅包含 開源"關鍵詞的第二個句子。第三個句子由於既不包含postgresql也不包含開源,因此被操作符@@略過。

      WITH sentence AS (
          SELECT 'PostgreSQL是世界上先進的開源關係型資料庫' AS content
          UNION ALL
          SELECT 'MySQL是應用廣泛的開源關聯式資料庫'
          UNION ALL
          SELECT 'MySQL在全球非常流行'
      )
      SELECT content,
             ts_rank(to_tsvector('jiebacfg', content), to_tsquery('jiebacfg', 'postgresql | 開源')) AS score
      FROM sentence
      WHERE to_tsvector('jiebacfg', content) @@ to_tsquery('jiebacfg', 'postgresql | 開源')
      ORDER BY score DESC;
      
                        content                   |    score
      --------------------------------------------+-------------
       PostgreSQL是世界上先進的開源關係型資料庫 |  0.06079271
       MySQL是應用廣泛的開源關聯式資料庫          | 0.030396355

      在極端情況下,例如遇到分詞問題或輸入包含錯誤字元時,可能會導致未能匹配到任何一篇文檔。為此,RDS PostgreSQL支援使用外掛程式模糊查詢(pg_bigm)進行降級匹配。通過執行bigm_similarity函數,返回最相似的文檔。例如:

      WITH sentence AS (
          SELECT 'PostgreSQL是世界上先進的開源關係型資料庫' AS content
          UNION ALL
          SELECT 'MySQL是應用廣泛的開源關聯式資料庫'
          UNION ALL
          SELECT 'MySQL在全球非常流行'
      )
      SELECT 
          content, 
          bigm_similarity(content, 'postgres | 開源產品') AS score 
      FROM 
          sentence
      ORDER BY 
          score DESC;
      
                        content                   |   score
      --------------------------------------------+------------
         PostgreSQL是世界上先進的開源關係型資料庫 | 0.23076923
         MySQL是應用廣泛的開源關聯式資料庫          | 0.05263158
         MySQL在全球非常流行                      |        0.0
      (3 行記錄)

      bigm_similarity函數將輸入的兩個文本轉換為2-gram元素,即連續兩個字元或兩個詞的組合。隨後,該Function Compute這兩個文本之間的共有元素個數,其取值範圍為[0, 1],其中1表示完全相同。因此,在處理分詞不準確、輸入存在拼字錯誤或部分縮寫等情況時,可以藉助pg_bigm外掛程式進行模糊查詢。詳情請參見模糊查詢(pg_bigm)

內容關鍵詞召回

對於文檔內容的檢索,可以使用和文檔關鍵詞召回相同的方式。然而,由於文檔內容的長度通常較關鍵詞更長,因此在對全文進行檢索時,建議使用外掛程式高速全文檢索索引(RUM)加速查詢過程。例如,可以使用以下查詢計劃,對使用者問題與文檔內容的關鍵詞進行相似性匹配。

  • RUM外掛程式基於GIN索引,儲存了額外的資訊,包括詞語的位置、時間戳記等。使用了RUM外掛程式的執行計畫,採用了embedding_rumidx索引,同時完成了條件過濾、相似性計算和排序,全部操作均通過索引實現,確保了查詢的效率。其執行時間為3.219ms。

    使用RUM外掛程式進行全文高速檢索

    EXPLAIN ANALYZE
    SELECT
        id,
        doc_id,
        content_chunk,
        ts_vector_extra <=> to_tsquery(
            REPLACE(
                TEXT(plainto_tsquery('jiebacfg', 'wal日誌堆積怎麼辦')),
                '&',
                '|'
            )
        ) AS similarity
    FROM
        embedding
    WHERE
        ts_vector_extra @@ to_tsquery(
            REPLACE(
                TEXT(plainto_tsquery('jiebacfg', 'wal日誌堆積怎麼辦')),
                '&',
                '|'
            )
        )
    ORDER BY
        similarity
    LIMIT
        10;
                                                                     QUERY PLAN
    --------------------------------------------------------------------------------------------------------------------------------------------
     Limit  (cost=10.15..22.14 rows=10 width=521) (actual time=3.117..3.182 rows=10 loops=1)
       ->  Index Scan using embedding_rumidx on embedding  (cost=10.15..6574.53 rows=5474 width=521) (actual time=3.115..3.179 rows=10 loops=1)
             Index Cond: (ts_vector_extra @@ to_tsquery('''wal'' | ''日誌'' | ''堆積'''::text))
             Order By: (ts_vector_extra <=> to_tsquery('''wal'' | ''日誌'' | ''堆積'''::text))
     Planning Time: 0.296 ms
     Execution Time: 3.219 ms
    (6 行記錄)
  • 使用原生GIN索引,基於冗餘的ts_vector_extra列,執行計畫相比於RUM要複雜得多。執行計畫啟用了兩個進程進行處理,並沒有採用RUM中的Index Scan,而是使用了Bitmap Index Scan。首先,通過索引擷取所有符合查詢條件的行的位元影像(bitmap),利用embedding_ts_vector_gin索引識別出相關行。在經過二次篩選後,確認了有效結果。隨後應用Top-N演算法進行排序,最後執行兩個進程的Gather Merge,以合并結果。其執行時間為14.234ms。

    使用原生GIN索引加速查詢

    EXPLAIN ANALYZE
    SELECT
        id,
        doc_id,
        content_chunk,
        ts_rank(
            ts_vector_extra,
            to_tsquery(
                REPLACE(
                    TEXT(plainto_tsquery('jiebacfg', 'wal日誌堆積怎麼辦')),
                    '&',
                    '|'
                )
            )
        ) AS similarity
    FROM
        embedding
    WHERE
        ts_vector_extra @@ to_tsquery(
            REPLACE(
                TEXT(plainto_tsquery('jiebacfg', 'wal日誌堆積怎麼辦')),
                '&',
                '|'
            )
        )
    ORDER BY
        similarity
    LIMIT
        10;
                                                                           QUERY PLAN
    ---------------------------------------------------------------------------------------------------------------------------------------------------------
     Limit  (cost=7178.59..7179.76 rows=10 width=520) (actual time=10.526..14.192 rows=10 loops=1)
       ->  Gather Merge  (cost=7178.59..7718.33 rows=4626 width=520) (actual time=10.525..14.189 rows=10 loops=1)
             Workers Planned: 2
             Workers Launched: 2
             ->  Sort  (cost=6178.57..6184.35 rows=2313 width=520) (actual time=6.879..6.880 rows=10 loops=3)
                   Sort Key: (ts_rank(ts_vector_extra, to_tsquery('''wal'' | ''日誌'' | ''堆積'''::text)))
                   Sort Method: top-N heapsort  Memory: 37kB
                   Worker 0:  Sort Method: top-N heapsort  Memory: 39kB
                   Worker 1:  Sort Method: top-N heapsort  Memory: 40kB
                   ->  Parallel Bitmap Heap Scan on embedding  (cost=56.47..6128.59 rows=2313 width=520) (actual time=0.567..6.367 rows=1637 loops=3)
                         Recheck Cond: (ts_vector_extra @@ to_tsquery('''wal'' | ''日誌'' | ''堆積'''::text))
                         Heap Blocks: exact=1515
                         ->  Bitmap Index Scan on embedding_ts_vector_gin  (cost=0.00..55.08 rows=5551 width=0) (actual time=0.794..0.794 rows=4910 loops=1)
                               Index Cond: (ts_vector_extra @@ to_tsquery('''wal'' | ''日誌'' | ''堆積'''::text))
     Planning Time: 0.291 ms
     Execution Time: 14.234 ms
  • GIN索引建立在to_tsvector('jiebacfg'::regconfig, content_chunk)上,和建立在ts_vector_extra列的執行計畫過程非常相似,WHERE的條件過濾兩者一致,而進入排序階段的行數也是類似的,主要區別在於ts_rank的計算。由於計算ts_rank需要使用詞彙的位置資訊,而在建立GIN索引時並不儲存位置資訊,因此在計算時必須對每一行重新計算to_tsvector,這一過程相對耗時。其執行時間為1081.547 ms。

    GIN索引建立在to_tsvector('jiebacfg'::regconfig, content_chunk)

    EXPLAIN ANALYZE
    SELECT
        id,
        doc_id,
        content_chunk,
        ts_rank(
            to_tsvector('jiebacfg', content_chunk),
            to_tsquery(
                REPLACE(
                    TEXT(plainto_tsquery('jiebacfg', 'wal日誌堆積怎麼辦')),
                    '&',
                    '|'
                )
            )
        ) AS similarity
    FROM
        embedding
    WHERE
        to_tsvector('jiebacfg', content_chunk) @@ to_tsquery(
            REPLACE(
                TEXT(plainto_tsquery('jiebacfg', 'wal日誌堆積怎麼辦')),
                '&',
                '|'
            )
        )
    ORDER BY
        similarity
    LIMIT
        10;
                                                                          QUERY PLAN
    -------------------------------------------------------------------------------------------------------------------------------------------------------
     Limit  (cost=8253.58..8254.75 rows=10 width=521) (actual time=1079.289..1081.510 rows=10 loops=1)
       ->  Gather Merge  (cost=8253.58..8786.55 rows=4568 width=521) (actual time=1079.287..1081.508 rows=10 loops=1)
             Workers Planned: 2
             Workers Launched: 2
             ->  Sort  (cost=7253.56..7259.27 rows=2284 width=521) (actual time=1073.189..1073.191 rows=10 loops=3)
                   Sort Key: (ts_rank(to_tsvector('jiebacfg'::regconfig, content_chunk), to_tsquery('''wal'' | ''日誌'' | ''堆積'''::text)))
                   Sort Method: top-N heapsort  Memory: 43kB
                   Worker 0:  Sort Method: top-N heapsort  Memory: 42kB
                   Worker 1:  Sort Method: top-N heapsort  Memory: 37kB
                   ->  Parallel Bitmap Heap Scan on embedding  (cost=55.93..7204.20 rows=2284 width=521) (actual time=2.127..1072.159 rows=1637 loops=3)
                         Recheck Cond: (to_tsvector('jiebacfg'::regconfig, content_chunk) @@ to_tsquery('''wal'' | ''日誌'' | ''堆積'''::text))
                         Heap Blocks: exact=1028
                         ->  Bitmap Index Scan on embedding_content_gin  (cost=0.00..54.56 rows=5481 width=0) (actual time=0.808..0.809 rows=4910 loops=1)
                               Index Cond: (to_tsvector('jiebacfg'::regconfig, content_chunk) @@ to_tsquery('''wal'' | ''日誌'' | ''堆積'''::text))
     Planning Time: 0.459 ms
     Execution Time: 1081.547 ms
    (16 行記錄)

BM25召回

BM25是一種經典的文本匹配演算法,綜合考慮了詞頻TF(Term Frequency)和逆向文檔頻率IDF(Inverse Document Frequency)的影響。從直觀上理解,詞頻TF越大,表示單詞在一篇文檔中出現的頻率越高,相關性越強;而逆向文檔頻率IDF越大,意味著單詞出現在越多的文檔中,從而其重要性越低。BM25演算法通過引入一些參數對TF-IDF演算法進行改進,以提升查詢效果。

本方案將BM25召回作為一種關鍵詞檢索方法,與RDS PostgreSQL的關鍵詞檢索進行對比。後者並未基於TF/IDF,而是以內建的ts_rank函數為例,僅考慮了查詢詞在文檔中出現的頻率、詞與詞之間的距離,以及它們在文檔各部分的重要性。因此,可以將BM25的檢索結果視為關鍵詞召回這一大類中的一種召回結果,從而提升整個系統的檢索準確度。

向量召回

RDS PostgreSQL支援外掛程式pgvector使用指南向量產生(rds_embedding)。 pgvector外掛程式提供了必要的向量資料類型支援和基礎向量操作能力,包括計算向量之間的距離和相似性等;而rds_embedding外掛程式則專註於將高維文本資料轉換為向量。相關外掛程式的詳細操作請參見pgvector使用指南向量產生(rds_embedding)

在RDS PostgreSQL中,向量類型的儲存確實可以通過數組來表示。然而,為什麼仍需定義資料類型為vector的向量類型呢?一個重要的原因在於,進行向量運算和排序時,如果未為向量類型建立相應的索引,將導致全表掃描和排序的開銷顯著增加。

pgvector外掛程式的索引構建支援兩種近似最近鄰搜尋(ANN)演算法的索引結構:HNSW和IVFFlat。由於HNSW索引無需先插入資料,並且查詢速度比IVFFlat索引快,因此在本方案中使用HNSW索引。

SELECT 
    embedding.id, 
    doc_id, 
    content_chunk, 
    content_embedding <=> '%s' AS similarity 
FROM 
    public.embedding 
LEFT JOIN 
    document ON document.id = embedding.doc_id 
WHERE 
    product_name = '%s' 
ORDER BY 
    similarity 
LIMIT %s;
說明

融合排序

本方案採用倒數排序融合(Reciprocal Rank Fusion, RRF)演算法及bce-reranker-base_v1,對文檔關鍵詞召回、內容關鍵詞召回、BM25召回和向量召回的結果進行精確排序。

  • RRF演算法的原理相對簡單易懂。根據以下公式,表示目的文件,表示文檔在第個系統的排名,是一個常數,可以選擇60或其他值。通過對個系統的排名值依次累加,最終得到結果。排名越靠前,取倒數後的值越大,因此計算出的RRF值也就越高。如果在多個系統中均名列前茅,那麼最終計算得到的RRF值將顯著增加。利用RRF演算法,可以對多路召回的chunks進行有效排序。

  • bce-reranker-base_v1是一種跨語言語義表徵演算法模型,專註於最佳化語義搜尋結果和相關性排序。該模型支援中文、英文、日文和韓文,具備較強的精排序能力。然而,在實際專案中,該模型的精排序時間相較於檢索召回階段較長,尤其是在候選chunks數量較多的情況下,處理時間會顯著增加,進而影響對話的流暢性體驗。因此,在RDS PostgreSQL工單機器人中,為實現更迅速的響應,可以採用RRF這種簡單高效的排序方法,或在RRF後續進行精確排序。若追求更高的準確性,則可以不使用RRF,直接應用bce-reranker-base_v1模型進行重排序。

問答分析

RDS PostgreSQL工單機器人針對不同的資料來源制定了相應的回答策略,以避免在調用大模型時,某些情況下對prompt_content內容進行非預期的處理。

  • 來源於知識庫的內容,不作為Prompt_content輸入給大模型進行加工,將直接輸出。

  • 來源於官方文檔的內容,由於擷取的HTML檔案經過分割後可能存在格式排版等問題,並且存在一些重複內容,因此利用大模型對這部分內容進行了歸納總結和格式化。

  • 來源於大模型的內容,僅在知識庫和官方文檔無法提供結果時,才會完全依賴大模型進行回答。從實踐經驗來看,直接調用大模型解決的工單問題相對較少,大模型更適合作為通用知識助手。

  • 來源於歷史工單的內容,僅輸出對應的工單名稱和連結。

回答的每一部分將列出相關文檔及其連結。推薦當根據回答內容無法解決問題時,建議查閱各個文檔原文,以擷取更全面和準確的知識內容,用於解決問題。

在測試階段,最近一個問答的評分可以作為對當前策略效果的初步評估。在代碼層面,支援編寫多個策略進行替換,以測試並確定最適合本產品的召回策略。

prompt = f'''請整理並格式化下面的內容並整理輸出格式,
        ```
        {prompt_content}
        ```
        基於自己的能力做出回答,我的問題是:{question}。
        '''

接入DingTalk機器人

在互動方式上,目前可以使用Streamlit搭建簡單網頁測試版,也可以接入DingTalk機器人。前者主要用於自測及文件管理等功能,後者則面向所有使用者提供使用。

在DingTalk群中每一次問答時,在資料庫層面均需發起一條新的串連。頻繁建立新串連不僅存在效能問題(包括時間和記憶體消耗),此外,如果未能及時釋放串連,可能導致串連數達到上限,從而使資料庫無法接受新的串連請求。在專案中採用串連池可以有效處理高頻短串連的情況,同時也可以直接利用RDS PostgreSQL內建的pgbouncer串連池功能。

使用樣本

本樣本通過簡單的多路召回,展示了RDS PostgreSQL在RAG中的強大和易用性。在本樣本中,使用者提出的問題為“介紹PostgreSQL”,由此可以獲得三種不同的召回方式的結果。

資料準備

  1. 在目標資料庫中,使用高許可權帳號執行如下SQL,安裝pg_jieba、pgvector、RUM和rds_embedding外掛程式。

    重要
    • 安裝pg_jieba外掛程式前,需要將pg_jieba添加到shared_preload_libraries的運行參數值中。修改shared_preload_libraries參數的取值,請參見設定執行個體參數

    • 您可以執行SELECT * FROM pg_extension;查看已安裝的外掛程式。

    CREATE EXTENSION IF NOT EXISTS pg_jieba;
    CREATE EXTENSION IF NOT EXISTS vector;
    CREATE EXTENSION IF NOT EXISTS rum;
    CREATE EXTENSION IF NOT EXISTS rds_embedding;
  2. 為RDS PostgreSQL執行個體所屬的VPC配置NAT Gateway,使其允許訪問外部模型。詳情請參見NAT Gateway配置

    說明

    RDS PostgreSQL資料庫預設不具備訪問外部網路的能力,因此,使用外部大模型,例如阿里雲大模型服務平台百鍊提供的通用文本向量模型時,需要為RDS PostgreSQL執行個體所屬的VPC配置NAT Gateway,使其允許訪問外部模型。

  3. 在目標資料庫中,執行如下SQL,建立測試表doc和embed,並為其建立相應的索引。

    --建立測試表doc及索引
    DROP TABLE IF EXISTS doc;
    
    CREATE TABLE doc (
        id bigserial PRIMARY KEY,
        title character varying(255) UNIQUE,
        key_word character varying(255) DEFAULT ''
    );
    
    CREATE INDEX doc_gin ON doc 
    USING GIN (to_tsvector('jiebacfg', key_word));
    
    --建立測試表embed及索引
    DROP TABLE IF EXISTS embed;
    
    CREATE TABLE embed (
        id bigserial PRIMARY KEY,
        doc_id integer,
        content text,
        embedding vector(1536),
        ts_vector_extra tsvector
    );
    
    CREATE INDEX ON embed 
    USING hnsw (embedding vector_cosine_ops) 
    WITH (
        m = 16, 
        ef_construction = 64
    );
  4. 執行如下SQL,建立觸發器,當embed表中的行被插入或更新時,自動更新ts_vector_extra列。

    -- 根據文本轉換成 tsvector,用於關鍵字的全文檢索索引
    CREATE TRIGGER embed_tsvector_update 
    BEFORE UPDATE OR INSERT 
    ON embed 
    FOR EACH ROW 
    EXECUTE PROCEDURE tsvector_update_trigger('ts_vector_extra', 'public.jiebacfg', 'content');
  5. 執行如下SQL,確保每次對embed表執行插入或更新操作時,都會根據新插入或更新的內容產生一個向量,並儲存在embedding列中。

    重要

    本樣本使用的模型為阿里雲大模型服務平台百鍊提供的通用文本向量模型,請先前往百鍊開通服務,並擷取API-KEY。具體操作,請參見擷取API Key

    -- 根據本文轉換成向量,注意替換api_key
    CREATE OR REPLACE FUNCTION update_embedding()
    RETURNS TRIGGER AS $$
    BEGIN
        NEW.embedding := rds_embedding.get_embedding_by_model('dashscope', 'sk-****', NEW.content)::real[];
        RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    CREATE TRIGGER set_embedding BEFORE INSERT OR UPDATE ON embed FOR EACH ROW EXECUTE FUNCTION update_embedding();
  6. 插入測試資料。

    INSERT INTO doc(id, title, key_word) VALUES 
    (1, 'PostgreSQL介紹', 'PostgreSQL 外掛程式'), 
    (2, 'MySQL介紹', 'MySQL MGR'), 
    (3, 'SQL Server介紹', 'SQL Server Microsoft');
    
    INSERT INTO embed(doc_id, content) VALUES 
    (1, 'PostgreSQL是以加州大學伯克利分校電腦系開發的POSTGRES,版本 4.2為基礎的對象關係型資料庫管理系統(ORDBMS)。POSTGRES領先的許多概念在很久以後才出現在一些商務資料庫系統中'), 
    (1, 'PostgreSQL是最初的伯克利代碼的開源繼承者。它支援大部分SQL標準並且提供了許多現代特性:複雜查詢、外鍵、觸發器、可更新視圖、事務完整性、多版本並發控制,同樣,PostgreSQL可以用許多方法擴充,比如,通過增加新的:資料類型、函數、操作符、聚集合函式、索引方法、過程語言'), 
    (1, '並且,因為自由寬鬆的許可證,任何人都可以以任何目的免費使用、修改和分發PostgreSQL,不管是私用、商用還是學術研究目的。'), 
    (1, 'Ganos外掛程式和PostGIS外掛程式不能安裝在同一個Schema下'), 
    (1, '豐富的生態系統:有大量現成的外掛程式和擴充可供使用,比如PostGIS(地理資訊處理)、TimescaleDB(時間序列資料庫)、pg_stat_statements(效能監控)等,能夠滿足不同情境的需要');
    
    INSERT INTO embed(doc_id, content) VALUES 
    (2, 'MySQL名稱的起源不明。10多年來,我們的基本目錄以及大量庫和工具均採用了首碼“my”。不過,共同創辦人Monty Widenius的女兒名字也叫“My”。時至今日,MySQL名稱的起源仍是一個迷,即使對我們也一樣'), 
    (2, 'MySQL軟體採用雙許可方式。使用者可根據GNU通用公用許可(http://www.fsf.org/licenses/)條款,將MySQL軟體作為開放源碼產品使用,或從MySQL AB公司購買標準的商業許可證。關於我方許可策略的更多資訊,請參見http://www.mysql.com/company/legal/licensing/。'), 
    (2, '組複製MySQL Group Replication(簡稱MGR)是MySQL官方在已有的Binlog複製架構之上,基於Paxos協議實現的一種分布式複製形態。RDS MySQL叢集系列執行個體支援組複製。本文介紹如何使複製方式為組複製。使用了組複製的MySQL叢集能夠基於分布式Paxos協議自我管理,具有很強的資料可靠性和資料一致性。相比傳統主備複製方式,組複製具有以下優勢:資料的強一致性,資料的強可靠性,全域事務強一致性');
    
    INSERT INTO embed(doc_id, content) VALUES 
    (3, 'Microsoft SQL Server是一種關聯式資料庫管理系統 (RDBMS)。應用程式和工具串連到SQL Server執行個體或資料庫,並使用Transact-SQL (T-SQL)進行通訊。'), 
    (3, 'SQL Server 2022 (16.x)在早期版本的基礎上構建,旨在將SQL Server發展成一個平台,以提供開發語言、資料類型、本地或雲環境以及作業系統選項。'), 
    (3, 'SQL Server在企業級應用中廣受歡迎,與其他Microsoft產品(如Excel、Power BI)無縫整合,便於資料分析');

多路召回

執行如下SQL,實現查詢文本“介紹一下postgresql”的多種檢索方式,並根據相似性對相關文檔進行排序。

--待查詢的問題,實際使用中請替換包
WITH query AS (
    SELECT '介紹一下postgresql' AS query_text
),
-- 將問題轉化為向量,sk-****請替換為百鍊的API-KEY
query_embedding AS (
    SELECT rds_embedding.get_embedding_by_model('dashscope', 'sk-****', query.query_text)::real[]::vector AS embedding
    FROM query
),
-- 基於文檔關鍵詞的搜尋,基於ts_rank,相似性越大得分越高
first_method AS (
    SELECT 
        id, 
        title, 
        ts_rank(to_tsvector('jiebacfg', doc.key_word), 
                to_tsquery(replace(text(plainto_tsquery('jiebacfg', (SELECT query_text FROM query))), '&', '|'))) AS score, 
        'doc_key_word' AS method
    FROM doc
    WHERE 
        to_tsvector('jiebacfg', doc.key_word) @@ 
        to_tsquery(replace(text(plainto_tsquery('jiebacfg', (SELECT query_text FROM query))), '&', '|'))
    ORDER BY 
        score DESC
    LIMIT 3
),
-- 基於文檔內容的關鍵詞全文檢索搜尋,基於rum的<=> 操作符,相似性越大得分越低
second_method AS (
    SELECT 
        id, 
        doc_id, 
        content, 
        to_tsvector('jiebacfg', content) <=> 
        to_tsquery(replace(text(plainto_tsquery('jiebacfg', (SELECT query_text FROM query))), '&', '|')) AS score,
        'content_key_word' AS method
    FROM embed
    WHERE 
        to_tsvector('jiebacfg', content) @@ 
        to_tsquery(replace(text(plainto_tsquery('jiebacfg', (SELECT query_text FROM query))), '&', '|'))
    ORDER BY 
        score 
    LIMIT 3
),
-- 基於向量的檢索,基於pgvector的操作符<=>,相似性越大得分越低
third_method AS (
    SELECT 
        embed.id, 
        embed.doc_id, 
        embed.content, 
        embedding <=> (SELECT embedding FROM query_embedding LIMIT 1) AS score, 
        'embedding' AS method
    FROM embed                 
    ORDER BY score
    LIMIT 3
)
-- join查詢得到更多欄位資訊
SELECT 
    first_method.title, 
    embed.id AS chunk_id, 
    SUBSTRING(embed.content FROM 1 FOR 30), 
    first_method.score, 
    first_method.method 
FROM first_method 
LEFT JOIN embed ON first_method.id = embed.doc_id
-- 將second_method聯合輸出
UNION
SELECT 
    doc.title, 
    second_method.id AS chunk_id, 
    SUBSTRING(second_method.content FROM 1 FOR 30), 
    second_method.score, 
    second_method.method 
FROM second_method 
LEFT JOIN doc ON second_method.doc_id = doc.id
-- 將third_method聯合輸出
UNION
SELECT 
    doc.title, 
    third_method.id AS chunk_id, 
    SUBSTRING(third_method.content FROM 1 FOR 30), 
    third_method.score, 
    third_method.method 
FROM third_method 
LEFT JOIN doc ON third_method.doc_id = doc.id
ORDER BY method, score;

返回結果:

     title      | chunk_id |                          substring                           |        score         |      method      
----------------+----------+--------------------------------------------------------------+----------------------+------------------
 PostgreSQL介紹 |        3 | PostgreSQL是最初的伯克利代碼的開源繼承者。它支援大           |   13.159472465515137 | content_key_word
 PostgreSQL介紹 |        2 | PostgreSQL是以加州大學伯克利分校電腦系開發的PO             |     16.4493408203125 | content_key_word
 PostgreSQL介紹 |        4 | 並且,因為自由寬鬆的許可證,任何人都可以以任何目的免費使用、 |     16.4493408203125 | content_key_word
 PostgreSQL介紹 |        6 | 豐富的生態系統:有大量現成的外掛程式和擴充可供使用,比如Post     | 0.020264236256480217 | doc_key_word
 PostgreSQL介紹 |        5 | Ganos外掛程式和PostGIS外掛程式不能安裝在同一個Schem                  | 0.020264236256480217 | doc_key_word
 PostgreSQL介紹 |        3 | PostgreSQL是最初的伯克利代碼的開源繼承者。它支援大           | 0.020264236256480217 | doc_key_word
 PostgreSQL介紹 |        2 | PostgreSQL是以加州大學伯克利分校電腦系開發的PO             | 0.020264236256480217 | doc_key_word
 PostgreSQL介紹 |        4 | 並且,因為自由寬鬆的許可證,任何人都可以以任何目的免費使用、 | 0.020264236256480217 | doc_key_word
 PostgreSQL介紹 |        2 | PostgreSQL是以加州大學伯克利分校電腦系開發的PO             |   0.2546271233144539 | embedding
 PostgreSQL介紹 |        3 | PostgreSQL是最初的伯克利代碼的開源繼承者。它支援大           |  0.28679098231865074 | embedding
 PostgreSQL介紹 |        6 | 豐富的生態系統:有大量現成的外掛程式和擴充可供使用,比如Post     |  0.41783296077761967 | embedding

相關文檔

更多基於RDS PostgreSQL的RAG最佳實務,請參見: