使用RDS PostgreSQL打造RAG应用

RDS PostgreSQL具备强大的向量存储与高效的全文检索能力,使其成为检索增强生成(RAG)应用的理想向量数据库,能够轻松实现基于关键词的检索。

文档导读

本文以基于RDS PostgreSQL构建专属工单机器人为例,介绍RDS PostgreSQL在构建RAG应用方面的优势。您将了解到:

  • 基于RDS PostgreSQL构建专属工单机器人的构建流程,以及在各个环节中RDS PostgreSQL相关插件及函数的使用方法。

  • 通过简单的多路召回使用示例,体验RDS PostgreSQL在RAG中的强大功能和易用性。

构建流程

基于RDS PostgreSQL构建专属工单机器人,整体流程可划分为:数据处理、多路召回、融合排序及问答分析。

  • 数据处理

    将源数据(例如官方文档、知识库、历史工单等)进行处理,包括切分和向量化(embedding),并将其存储至RDS PostgreSQL数据库中。

  • 多路召回

    • 文档关键词召回:对问题与文档表中的关键词进行相似度匹配,返回匹配度最高的前N篇文档。

    • 内容关键词召回:基于文档内容的关键词进行检索,对用户问题与文档内容的关键词进行相似度匹配。

    • BM25召回:基于词频、词与词之间的距离,以及它们所归属文档模块的重要性,对相关性评分进行统计分析,返回评分靠前的内容。

    • 向量召回:将问题转换为向量形式,并计算其余弦相似度,返回相似度靠前的内容。

  • 融合排序

    对多路召回的结果,采用倒数排序融合(Reciprocal Rank Fusion, RRF)算法及开源的bce-reranker-base_v1模型进行精确排序。

  • 问答分析

    将问题及其答案存储于数据库中,在测试阶段对问答效果进行评分,以评估不同检索策略的效果。

数据处理

数据处理是指对源数据(例如官方文档、知识库、历史工单等)进行处理,包括切分和向量化(embedding),并将其存储至RDS PostgreSQL数据库中。

获取数据

请根据RAG应用的目的获取相关数据。本文以RDS PostgreSQL钉钉工单机器人为例,收集了相应的帮助文档、知识库及历史工单。

处理数据

对源数据进行处理,按照特定格式保存到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><3>表示前后两个词汇之间的间距,而<->则表示相邻的关系,例如开源关系需相邻出现。此外,常见的停用词(例如,等)将被自动移除。除此之外,还有其他常用符号:&表示并且,|表示或者,!表示非,同时也可以加权表示,例如:

      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元素,即连续两个字符或两个词的组合。随后,该函数计算这两个文本之间的共有元素个数,其取值范围为[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}。
        '''

接入钉钉机器人

在交互方式上,目前可以使用Streamlit搭建简单网页测试版,也可以接入钉钉机器人。前者主要用于自测及文档管理等功能,后者则面向所有用户提供使用。

在钉钉群中每一次问答时,在数据库层面均需发起一条新的连接。频繁建立新连接不仅存在性能问题(包括时间和内存消耗),此外,如果未能及时释放连接,可能导致连接数达到上限,从而使数据库无法接受新的连接请求。在项目中采用连接池可以有效处理高频短连接的情况,同时也可以直接利用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网关,使其允许访问外部模型。详情请参见NAT网关配置

    说明

    RDS PostgreSQL数据库默认不具备访问外部网络的能力,因此,使用外部大模型,例如阿里云大模型服务平台百炼提供的通用文本向量模型时,需要为RDS PostgreSQL实例所属的VPC配置NAT网关,使其允许访问外部模型。

  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)
    );
    
    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最佳实践,请参见: