PostgreSQL码农集散地

PDF、扫描图像RAG效果不佳的解决之道

本期播客

多向量搜索来了 | 无需OCR, PDF、扫描文档RAG效果不佳的解决之道

VectorChord 是一款PostgreSQL向量索引插件, 不过相比于PG其他向量索引插件, VectorChord的优势是采用了rabitq量化、残差量化、向量数组检索(支撑本文场景的关键技术)、外部build、ivfflat、图、等等新颖技术, 优势包括

  • 性能更强
  • 更省内存
  • 索引更小
  • 索引和搜索时的可调参数非常多
  • 支持maxsim算子等

本文将介绍一种场景: 当RAG资料库不仅有纯文本, 还有pdf、扫描文档、图片等需要OCR才能识别的素材时(而且OCR并不完美,导致信息割裂), 构建RAG系统将变得无比复杂, 效率低下.

结合 Modal 和 PostgreSQL+VectorChord 可解锁无 OCR 的 RAG, 无惧PDF、扫描文档

以下内容翻译自: https://blog.vectorchord.ai/beyond-text-unlock-ocr-free-rag-in-postgresql-with-modal-and-vectorchord

超越文本: 使用 Modal 和 PostgreSQL+VectorChord 解锁无 OCR 的 RAG, 无惧PDF、扫描文档等

构建高效的文档检索增强生成 (RAG) 系统,往往感觉像是在与杂乱复杂的流程搏斗。尤其是在处理 PDF 或扫描图像时,传统方法严重依赖光学字符识别 (OCR) 和布局(layout)分析。这些步骤可能导致速度缓慢、容易出错,并且经常丢失表格、图表和格式等关键的视觉信息。但是,如果您可以根据文档的外观(而不仅仅是提取的文本)来查询文档,情况会怎样呢?

这篇文章将指导您如何构建一个无需 OCR 的 RAG 系统,它直接在您熟悉的 PostgreSQL 数据库中运行。我们将利用 ColQwen2 视觉语言模型的强大功能、 VectorChord 在 Postgres 中执行多向量搜索(maxsim)的高效性,以及Modal基于 GPU 的embedding生成能力的可扩展性。准备好简化您的 RAG 堆栈,并潜在地提升您的检索准确率,所有这些都无需复杂的预处理。

我们将介绍:

  • ColQwen2 是什么以及它为何能改变游戏规则。
  • VectorChord 如何在 Postgres 中实现高级向量搜索(maxsim)。
  • 构建和评估系统的分步教程。

ColQwen2 是什么?视觉理解的力量

要理解 ColQwen2,我们先来了解一下它的基础:ColPali。正如论文《ColPali:基于视觉语言模型的高效文档检索》中介绍的那样,ColPali 代表了一种使用视觉语言模型 (VLM) 的全新方法。它不再依赖于不完善的 OCR,而是直接利用文档的视觉特征(文本、图像、表格、布局以及眼睛能看到的一切)来索引文档。

想想传统 OCR 的局限性:复杂的布局会变得混乱,表格会变得乱七八糟,图像经常被完全忽略。这就像试图通过阅读有缺陷的抄本来理解一本书。ColPali 通过使用强大的 VLM(最初是 PaliGemma)来创建能够捕捉文档整体视觉特征的embedding,从而避免了这种情况。两个关键概念使其脱颖而出:

  • 情境化(context)视觉embedding:使用 VLM 直接从文档图像生成信息量更丰富的embedding。
  • 后期交互:这项巧妙的技术允许query的文本含义在搜索时直接与文档的详细视觉特征进行交互。它不仅仅是匹配文本摘要;它还将query与文档页面中的视觉信息进行比较。


Image

ColPali架构(图片来自 ColPali 论文)

ColQwen2 建立在强大的 ColPali 架构之上,但将底层 VLM 替换为较新的 Qwen2-VL-2B(现在可能又有更新、参数量更大的版本了)。它生成ColBERT风格的多向量表征,能够捕捉文本和图像中的细粒度细节。正如vidro-leaderboard所示, ColQwen2 以实用的模型尺寸提供了令人印象深刻的性能。

VectorChord 如何在 Postgres 中启用 ColQwen2?

VectorChord成为了关键要素,将这一尖端的 VLM 功能引入了您的 PostgreSQL 数据库。ColQwen2(以及 ColPali)高度依赖于这些多向量表示和后期交互机制,尤其需要高效的MaxSim(最大相似度)操作。

计算 MaxSim(即在查询集中的任意向量与文档集中的任意向量之间找到最高相似度得分)在计算上可能非常复杂(类似笛卡尔乘机的运算量),尤其是在处理数百万个文档向量时。VectorChord 则直接解决了这个问题:

  • 库内直接支持多向量(即向量数组类型):它从头开始设计,以便在 Postgres 中有效地处理多向量数据。
  • 优化的 MaxSim:从WARP 论文中汲取灵感,VectorChord 使用动态相似性插补等技术显著加快 MaxSim 计算速度,从而实现高效、大规模视觉文档检索。
  • 混合搜索就绪:除了多向量之外,它还支持密集、稀疏和混合搜索( 查看我们之前的帖子! )。
  • 可扩展且磁盘友好:专为性能而设计,无需过多的资源。索引记录可超过内存大小.

简而言之,VectorChord 将 PostgreSQL 转变为一个强大的向量搜索引擎,能够处理 ColQwen2 或 ColPali 等模型所需的高级向量搜索技术。

教程:构建无 OCR 的 RAG 系统

好吧,理论很棒,但让我们撸起袖子,动手做吧!我们将逐步讲解环境搭建、使用 Modal 处理数据以生成可扩展的embedding、在 Postgres 中建立 VectorChord 索引,以及最终评估全新且无 OCR 的 RAG 系统。

先决条件

在开始之前,请确保您已:

  • 安装了 VectorChord 扩展的 PostgreSQL 实例(推荐使用 Docker)或VectorChord Cloud集群。
  • 一个Modal帐户(提供免费套餐)。Modal 的快速 GPU 配置和扩展功能非常适合embedding生成步骤。为了使用 ColQwen2 模型高效处理大量文档,充分利用 Modal 的快速启动和 GPU 扩展功能至关重要。这种方法将显著缩短本地处理所需的时间。

如果您想快速重现本教程,您可以使用该 tensorchord/vchord-suite docker 镜像运行 TensorChord 提供的多个扩展。

您可以运行以下命令使用 VectorChord-BM25 和 VectorChord 构建和启动 Postgres。

docker run   \             
  --name vchord-suite  \  
  -e POSTGRES_PASSWORD=postgres  \  
  -p 5432:5432 \  
  -d tensorchord/vchord-suite:pg17-latest  
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;  
CREATE EXTENSION IF NOT EXISTS pg_tokenizer CASCADE;  
CREATE EXTENSION IF NOT EXISTS vchord_bm25 CASCADE;  
\dx  
pg_tokenizer | 0.1.0   | tokenizer_catalog | pg_tokenizer  
vchord       | 0.3.0   | public            | vchord: Vector database plugin for Postgres, written in Rust, specifically designed for LLM  
vchord_bm25  | 0.2.0   | bm25_catalog      | vchord_bm25: A postgresql extension for bm25 ranking algorithm  
vector       | 0.8.0   | public            | vector data type and ivfflat and hnsw access methods  

设置 modal:

$ pip install modal  
$ python3 -m modal setup  # click the link to authorize  

步骤 1:加载数据(Using Modal Volumes)

我们将使用 ViDoRe 基准数据集。为了在 Modal 函数之间高效处理这些数据,我们将它下载到 Modal 卷中。卷提供持久的共享存储,非常适合这种“一次下载,多次处理”的场景。

image = modal.Image.debian_slim().pip_install("datasets","huggingface_hub","Pillow")  
DATASET_DIR = "/data"
DATASET_VOLUME  = modal.Volume.from_name(  
"colpali-dataset", create_if_missing=True  
)  
app = modal.App(image=image)  

@app.function(volumes={DATASET_DIR: DATASET_VOLUME}, timeout=3000)  
def download_dataset(cache=False) -> None:  
    from datasets import load_dataset  
    from tqdm import tqdm  

    collection_dataset_names = get_collection_dataset_names("vidore/vidore-benchmark-667173f98e70a1c0fa4db00d")   
for dataset_name in tqdm(collection_dataset_names, desc="vidore benchmark dataset(s)"):  
        dataset = load_dataset(dataset_name, split="test",num_proc=10)  
        unique_indices = dataset.to_pandas().drop_duplicates(subset="image_filename", keep="first").index #to remove repeating PDF pages with different queries  
        dataset = dataset.select(unique_indices)  
        dataset.save_to_disk(f"{DATASET_DIR}/{dataset_name}")  

Modal 提供了一套简洁易用的 Python API,方便用户与其平台交互。您可以使用modal.Image来设置 Modal 应用的基础镜像,function装饰器则可以帮助您定义要在 Modal 应用程序中执行的函数。要将数据集下载到 Modal 卷,请执行以下命令。Modal 将自动构建 Docker 镜像并立即启动容器来运行您的函数。

运行下载功能:

$ modal run dataset.py::download_dataset  

Modal 负责在云中构建环境和运行脚本。

第 2 步:处理数据并生成 embedding(使用 Modal 和 ColQwen2)

这是一项繁重的工作:将文档图像转换为 ColQwen2 多向量 embedding。对于大量文档,在本地执行此操作会很慢。Modal 在这里表现出色:

  • 轻松的 GPU 访问和自动扩展:在 Modal 上启动 ColQwen2 模型服务,并使用它来生成数据集的图像 embedding 和 查询 embedding。我们决定实现 HTTP 服务而不是直接使用 SDK,以确保在大规模 embedding 操作期间实现无缝的自动扩展。如果您想将 embedding 服务部署为持久 Web 端点,可以直接切换modal.cls到modal.web_serverColPaliServer 并运行modal deploy。
  • 恢复:我们将进度检查点设置到单独的模态卷,以便在发生中断时恢复embedding的生成。
# embedding.py (Illustrative - keep original code)  
# ... imports, volumes setup ...  
modal_app = modal.App() # Define app  

# Function to coordinate embedding generation  
@modal_app.function(...)  
def embed_dataset(down_scale: float = 1.0, batch_size: int = BATCH_SIZE):  
# ... (logic for loading dataset names, handling checkpoints) ...  
    colpali_server = ColPaliServer() # Get handle to our GPU class  
# ... (loop through datasets, batch items, call server for embeddings) ...  
# ... (save embeddings and update checkpoint) ...  
print("Embedding generation complete.")  

# server.py (Illustrative - keep original code)  
# Class running on GPU to serve embedding requests  
@modal_app.cls(gpu=GPU_CONFIG, ...)  
class ColPaliServer:  
    @modal.enter()  
    def load_model_and_start_server(self):  
# ... (Load ColQwen2 model, start internal FastAPI server) ...  
        self.client = httpx.AsyncClient(...) # Client to talk to internal server  

    @modal.exit()  
    def shutdown_server(self):  
# ... (Cleanup) ...  

# Method called by embed_dataset function  
    @modal.method()  
    async def embed_images(self, images: List[str]) -> np.ndarray:  
# ... (Prepare batch, send request to internal server via self.client) ...  
# ... (Decode response using msgspec for speed) ...  
return embeddings_numpy_array  

# ... (Potentially add embed_queries method too) ...  

# colpali.py  
class ColPaliModel:  
    def __init__(self, model_name: str = "vidore/colqwen2-v1.0", cache_dir: str="/model"):  
# ...  
if self.model_name == "vidore/colqwen2-v1.0":  
            from colpali_engine.models import ColQwen2, ColQwen2Processor  
            from transformers.utils.import_utils import is_flash_attn_2_available  

# load model  
            model = ColQwen2.from_pretrained(  
                self.model_name,  
                torch_dtype=torch.bfloat16,  
                device_map="cuda:0",   
                attn_implementation="flash_attention_2"if is_flash_attn_2_available() else None,  
                cache_dir=self.cache_dir,  
            ).eval()  

            colpali_processor = ColQwen2Processor.from_pretrained(  
                self.model_name,  
                cache_dir=self.cache_dir,  
            )  
# ... functions for embedding images and queries  
# @modal.method()  
# async def batch_embed_images(self, images_base64: List[str]) -> np.ndarray:  
# @modal.method()  
# async def batch_embed_queries(self, queries: List[str]) -> np.ndarray:  

生成 embedding:

$ modal run embedding.py::embed_dataset  

步骤 3:在 VectorChord 中创建索引(使用 Vechord SDK)

在Modal Volume中生成并保存 embedding 后,我们首先需要将它们本地化(下载到本地)。

$ modal volume get colpali-embedding-checkpoint /path/to/local/vidore_embeddings  

现在,我们将使用vechord SDK 将这些embedding加载到我们的 PostgreSQL 数据库中并创建必要的多向量索引(maxsim)。vechord 提供了一种类似 Pythonic ORM 的方式来与 Postgres 中的 VectorChord 进行交互。

https://github.com/tensorchord/vechord

MultiVector = List[Vector[128]] # Assuming 128 dimensions for ColQwen2  

lists = 2500 # lists is the number of the cluster  
# Define the database table schema using vechord  
class Image(Table, kw_only=True):  
    uid: Optional[PrimaryKeyAutoIncrease] = None  
    image_embedding: Annotated[MultiVector, MultiVectorIndex(lists=lists)] # Stores the document's visual embeddings   
    query_embedding: Annotated[MultiVector, MultiVectorIndex(lists=lists)]  # Stores the query's embeddings   
    query: str = None  
    dataset: Optional[str] = None  
    dataset_id: Optional[int] = None  

# Connect to your PostgreSQL database  
# Ensure the URL points to your local Docker or VectorChord Cloud instance  
DB_URL = "postgresql://postgres:[email protected]:5432/postgres"# Default DB  
vr = VechordRegistry("colpali", DB_URL)  
vr.register([Image]) # Creates the table and multi-vector index if they don't exist  

# Function to load embeddings from disk and yield Image objects  
@vr.inject(output=Image)  
def load_image_embeddings(path: str) -> Iterator[Image]:  
# ... (logic to load numpy arrays from disk, convert to List[Vector]) ...  
# ... (yield Image(...) instances) ...  
print(f"Loaded embeddings from {path}")  

if __name__ == "__main__":  
# Path where you downloaded the embeddings from Modal  
    embedding_dir = "/path/to/local/vidore_embeddings"
    load_image_embeddings(embedding_dir) # This triggers vechord to insert data  
print("Data loaded and indexed into VectorChord.")  

运行索引器脚本,vechord 将处理表创建、索引创建和数据插入。

步骤4:评估 -- 它有效吗?

最后一步至关重要:评估检索性能。

这个无需 OCR 的系统能否提供准确的结果?

它的速度是否足够快?

我们将使用 vechord 对索引数据运行查询,并查看 NDCG@10 和 Recall@10。我们还将测试 VectorChord 采用了 WARP 优化的效果,该优化可以加速 MaxSim 的计算。在本教程中,我们将使用vidore/arxivqa_test_subsampled数据集进行评估查询,数据如下所示。



Image

TOP_K = 10  

# Define structure for results (optional but good practice)  
class Evaluation(msgspec.Struct):  
    map: float
    ndcg: float
    recall: float

TOP_K = 10  
def evaluate(queries: list[Image], probes: int, max_maxsim_tuples: int) -> list[Evaluation]:  
    result  = []  
for query in queries:  
        vector = query.query_embedding  
        docs: list[Image] = vr.search_by_multivec(  
            Image, vector, topk=TOP_K, probe=probes, max_maxsim_tuples=max_maxsim_tuples  
        )  
        score = BaseEvaluator.evaluate_one(query.uid, [doc.uid for doc in docs])  
        result.append(Evaluation(  
            map=score.get("map"),  
            ndcg=score.get("ndcg"),  
            recall=score.get(f"recall_{TOP_K}"),  
        ))  
return result  

if __name__ == "__main__":  
# Select some queries from the benchmark dataset stored in the DB  
# Example: Get 100 queries from a specific subset  
    test_queries: list[Image] = vr.select_by(  
        Image.partial_init(dataset="vidore/arxivqa_test_subsampled"), limit=100  
    )  

# ... optimize the param ...  
    res: list[Evaluation] = evaluate(queries, probes=probes, maxsim_refine=maxsim_refine)  
print("ndcg@10", sum(r.ndcg for r in res) / len(res))  
print("recall@10", sum(r.recall for r in res) / len(res))  
print(f"Total execution time: {total_time:.4f} seconds")  

运行评估脚本产生了令人印象深刻的结果:

# Disable WARP  
ndcg@10 0.8615  
recall@10 0.92  
Total execution time: 810 seconds # Baseline accuracy and time  

# Enable WARP  
# There is no need to focus on specific times,   
# only relative times need to be taken into account,   
# as the tests were performed on a local model with poor performance.  
ndcg@10 0.8353  
recall@10 0.90  
Total execution time: 41 seconds # Dramatic WARP Speed Boost  

分析:

  • 高基线准确率:首先,我们来看看未进行 WARP 优化的基线性能。系统实现了出色的 NDCG@10 准确率 (0.8615)和 Recall@10 准确率 (0.92) 。这证实了 ColQwen2 embedding 与 VectorChord 精准的 MaxSim 搜索相结合的有效性,即使没有速度优化,也能为视觉文档检索提供最佳结果。

  • WARP 速度显著提升:现在,观察启用 VectorChord WARP 优化后的效果。总执行时间从 810 秒 骤降至仅 41 秒 !这意味着评估查询集的速度提升了约 18.7 倍。这清楚地证明了 WARP 在显著加速 ColQwen2 等后期交互模型所需的计算密集型 MaxSim 操作方面的强大功能。

  • 最小准确度代价:令人印象深刻的是,如此显著的速度提升却以检索准确度的轻微下降为代价。NDCG@10 略微下降至0.8353(差异小于 0.3),而 Recall@10 则略微下降至0.9(差异仅为 0.02)。

结论:Postgres 中的 Visual RAG 更加简单

在本教程中,我们成功构建了一个高性能、无 OCR 的 RAG 系统,该系统整合了 ColQwen2 的视觉理解功能、 PostgreSQL VectorChord插件的可扩展多向量搜索功能 以及 Modal的高效 GPU 处理能力。我们了解了该技术栈允许我们直接根据文档的视觉内容进行查询,从而绕过了传统 OCR 流程的缺陷。

在 Postgres 中将这种视觉维度直接无缝集成到您的 RAG 系统中的能力为处理视觉丰富的文档(如科学论文、发票、产品手册、历史档案等)的应用程序开辟了令人兴奋的可能性。

准备好亲自尝试一下了吗?

  • 探索代码: https://github.com/xieydd/vectorchord-colqwen2

  • 深入了解 VectorChord:查看VectorChord 文档或尝试无忧的VectorChord Cloud。

  • 实验:尝试不同的 VLM 或数据集。

  • 分享您的想法:在下面的评论中让我们知道您的经历或问题!

这种方法代表着 RAG 向更简单、更强大、视觉感知的文档检索系统迈出了重要一步。

参考

  • https://huggingface.co/vidore/colqwen2-v1.0

  • https://huggingface.co/blog/manu/colpali

  • https://blog.vespa.ai/scaling-colpali-to-billions/

参考

https://blog.vectorchord.ai/beyond-text-unlock-ocr-free-rag-in-postgresql-with-modal-and-vectorchord

https://deepwiki.com/tensorchord/VectorChord/

最后附一则济南的线下峰会消息:
PostgreSQL & IvorySQL 2026 年度峰会将于4月份在济南召开,这是目前国内规模最大的PG峰会. 
我是新特性分论坛出品人,欢迎报名参与分享,主委会可解决分享嘉宾住宿和路费。
报名地址:  https://jsj.top/f/uebqBc

议题方向:

  • PostgreSQL 新功能
  • PostgreSQL 内核机制与性能优化
  • PostgreSQL 扩展程序
  • AI + PostgreSQL 技术实践
  • 云原生PostgreSQL或IvorySQL
  • PostgreSQL 用户实践
  • IvorySQL 兼容性与生态实践
  • 基准测试与性能调优
  • 高可用性技术
  • …… 任何与PostgreSQL或IvorySQL相关的内容