iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
AI & Data

論文流浪記:我與AI 探索工具、組合流程、挑戰完整平台系列 第 6

Day 5|資料的家:MinIO , Qdrant, PostgreSQL 的故事 — 文件與資料庫

  • 分享至 

  • xImage
  •  

前言

今天,我的冒險船再次啟航,目標是將前幾天辛苦收集的 arXiv 論文寶藏,安全地存放進兩座寶庫——PostgreSQL 與 Qdrant。這就像探險者在遼闊的荒原上,不只要找到寶藏,還得把它妥善分類、標記,才能在日後快速取出使用。上一回我們只是抓到寶藏與打開 PDF,今天則要學會「編號、上鎖、地圖標記」的完整流程。

前方的挑戰明顯:process_pdfs_task 將資料送往 PostgreSQLMinIO(可選),而 qdrant_index_task 將文字 chunk 轉向量後存入 Qdrant。雖然之前只簡單提過,今天我將揭開這三個工具的神秘面紗,以及我為什麼選它們。

PostgreSQL 管 metadata,Qdrant 管語意檢索,MinIO 管檔案備份。三者合力,才構成完整的知識寶庫。

小提醒:安裝完 PostgreSQL 和 Qdrant 後,就可以打開 Qdrant Dashboard (http://localhost:6333/dashboard) 欣賞它精美的 UI,並搭配 DBeaver 觀察 PostgreSQL。
之後會再說明安裝細節。

為什麼選 PostgreSQL、Qdrant 與 MinIO

在資料工程世界,每一篇論文都是一個珍貴寶物。

PostgreSQL:結構化的寶箱
PostgreSQL 就像探險者的寶箱,用結構化方式保存每篇論文的 metadata、PDF 原文(可選)以及解析後的章節和參考文獻。透過 schema 設計,我可以輕鬆查詢作者、分類、發表日期,還能記錄 PDF 處理狀態,就像在寶箱上貼標籤📌。

  • 技術層面:PostgreSQL 提供強大的結構化查詢能力,支援 JSON、Array、Date 等型別,方便做關聯查詢和全文檢索,讓資料庫不只是收納工具,更像是知識管理中心。
  • PostgreSQL 的 GIN + tsvector 能實作全文檢索,和向量檢索互補:前者適合關鍵字精準匹配,後者適合語意近似搜尋。PostgreSQL 解答「這是誰的寶藏、何時放進來」,Qdrant 解答「這跟哪個寶藏最像」。
    一個是結構化關聯,一個是語意相似性。

Qdrant:智慧的魔法地圖
Qdrant 則像魔法地圖,不只記住寶藏位置,還能理解寶藏之間的相似性。每篇論文會被切成文字 chunk,再透過 NLP embedding 轉成 768 維向量,儲存在 Qdrant。當我想找「強化學習」相關論文時,魔法地圖能迅速指出最相似的 chunk,省去翻遍整個資料海洋的痛苦🔮。

技術層面:向量資料庫 + Cosine similarity,可以做語意搜尋而非單純文字比對,尤其在多 chunk 文件或跨領域搜尋時威力無窮。

MinIO:可擴展的檔案倉庫
MinIO 是可選的倉庫,用來存放原始 PDF(或 png/html,雖然 arXiv 官方不建議😅)。即便 PostgreSQL 只存文字內容,原始檔案仍有備份,確保資料完整性。想像它是探險者的防水袋🛡️💧——怕寶物淋濕或丟失。

就是沒錢上雲端拉 💸,只能自己背著走,但寶物還是安全的。
當資料量成長到數十萬篇論文時,PDF 原檔會佔據大量空間,這時 MinIO 的分散式物件儲存優勢才會顯現。

工具 功能定位 比喻
PostgreSQL 結構化查詢 寶箱 📦
Qdrant 向量檢索 魔法地圖 🗺️
MinIO 原始檔案儲存 防水袋 💧

寶藏地圖的建立與標記

postgresql schema 設計


CREATE TABLE papers (
    id SERIAL PRIMARY KEY,
    arxiv_id VARCHAR(32) UNIQUE NOT NULL,
    title TEXT NOT NULL,
    authors TEXT [] NOT NULL,
    abstract TEXT,
    categories TEXT [],
    published_date DATE,
    updated_date DATE,
    pdf_url TEXT,
    -- Parsed PDF content
    raw_text TEXT,
    sections JSON,
    "references" JSON,
    -- PDF processing metadata
    parser_used VARCHAR,
    parser_metadata JSON,
    pdf_processed BOOLEAN NOT NULL DEFAULT FALSE,
    pdf_processing_date TIMESTAMP,
    -- Timestamps
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 可加索引,加速查詢
CREATE INDEX idx_papers_published_date ON papers (published_date);

CREATE INDEX idx_papers_categories ON papers USING GIN (categories);

CREATE TABLE user_sent_papers (
    id SERIAL PRIMARY KEY,
    user_id VARCHAR(255) NOT NULL,
    arxiv_id VARCHAR NOT NULL,
    sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_user_sent_papers_user_id ON user_sent_papers (user_id);

每個欄位就像寶箱的小格子,作者、標題、摘要、PDF 處理狀態都標示清楚,方便快速檢索。

Qdrant schema 設計

point = models.PointStruct(
    id=idx,                  # 唯一識別 ID (int 或 str)
    vector=vector,           # 向量表示 (list[float], 例如 768 維)
    payload={                # 附加的 metadata
        "arxiv_id": paper.arxiv_id,           # 論文唯一 ID
        "title": paper.title,                 # 論文標題
        "abstract": paper.abstract,           # 摘要
        "authors": paper.authors,             # 作者列表
        "categories": paper.categories,       # arXiv 分類
        "published_date": paper.published_date, # 發表日期
        "text": chunk,                         # 文字 chunk
        "chunk_idx": chunk_idx                 # chunk 在全文的索引位置
    }
)

這個唯一 ID 就像寶藏的標籤,確保每個 chunk 的位置唯一,方便未來查找或更新。

📚 傳統 SQL 資料庫的索引

  • 目的:加速查詢 WHEREJOIN 等條件匹配
  • 實作:常見是 B-TreeHash Index
  • 特性:資料存表裡,索引是額外的輔助結構

🔍 向量資料庫的索引

  • 目的:加速「相似度搜尋」(Approximate Nearest Neighbor, ANN)

  • 流程

    1. 把資料(文字、圖片…)轉成 embedding 向量
    2. 把向量寫進資料庫(例如 Qdrant 的 upsert
    3. 資料庫內部會建 相似度索引結構(HNSW、IVF、PQ…)
  • 特性

    • 「存資料」本身就等於「進索引」
    • 不需要額外 CREATE INDEX(除非要調參或換演算法)

在 Qdrant 這種向量資料庫裡:

  • 你呼叫 upsert() → 資料進 collection → 系統自動放進 HNSW 索引
  • 之後查詢 search() → 就是走這個 HNSW graph

換句話說:在 SQL 裡「索引是額外的結構」;在向量 DB 裡「索引就是存放資料的方式」


CRUD 操作:管理寶藏

連線 Qdrant


from qdrant_client import QdrantClient, models
from config import COLLECTION_NAME, QDRANT_URL

qdrant_client = QdrantClient(url=QDRANT_URL, timeout=300)

1️⃣ Create / 建立 Collection

初始化資料集


# 建立 collection(Cosine 距離,向量維度 768)
qdrant_client.recreate_collection(
    collection_name=COLLECTION_NAME,
    vectors_config=models.VectorParams(
        size=768,
        distance=models.Distance.COSINE
    ),
)
print(f"Collection '{COLLECTION_NAME}' created.")

指定向量維度與距離函數(Cosine similarity),確保搜尋時能正確排序。

  • 文本向量(像 SentenceTransformer、BERT) → Cosine 最常用
  • 有方向或大小很重要 → Dot
  • 坐標型或實數向量 → Euclid 或 Manhattan

2️⃣ Upsert Points

每次新論文進來或更新時

from services.embedding import get_embedding

# 範例:將一篇論文的 chunk 轉向量
vector = get_embedding(chunk)  # chunk 是文字片段

point = models.PointStruct(
    id=f"{paper.arxiv_id}_{chunk_idx}",  # 唯一 ID
    vector=vector,
    payload={
        "arxiv_id": paper.arxiv_id,
        "title": paper.title,
        "abstract": paper.abstract,
        "authors": paper.authors,
        "categories": paper.categories,
        "published_date": paper.published_date,
        "text": chunk,
        "chunk_idx": chunk_idx
    }
)

# 上傳到 Qdrant
qdrant_client.upsert(collection_name=COLLECTION_NAME, points=[point])
print("Point upserted.")

Tips: 可以累積一批 points,再一次上傳,提高效率。
Qdrant 預設使用 HNSW 索引來維護 collection,因此每次 upsert() 就等於把 chunk 插入到 HNSW graph。
在 Qdrant 裡,collection 內部的 HNSW graph 本身就是索引。
這不像 PostgreSQL 可以先建表、再決定要不要加索引;在 Qdrant,資料一進來就會被插入 HNSW。

在資料庫或向量資料庫(像 Qdrant)裡,UPSERT 就是 INSERT + UPDATE 的組合:

  • 如果資料不存在 → 做 INSERT,把新資料加進去。
  • 如果資料已經存在(通常透過唯一 ID 判斷) → 做 UPDATE,更新現有資料的內容。

UPSERT = INSERT + UPDATE
沒寶藏 → 放新寶物
已有寶藏 → 補充或替換


3️⃣ Read / 搜尋 Points

檢索語意相關 chunk

from qdrant_client.http.models import Filter, FieldCondition, MatchValue

# 先建立 metadata filter(可選)
metadata_filter = Filter(
    must=[
        FieldCondition(key="arxiv_id", match=MatchValue(value=paper.arxiv_id))
    ]
)

# 計算查詢文字向量
query_vector = get_embedding("需要搜尋的文字")

# 搜尋 top_k 最相似的 points
search_result = qdrant_client.search(
    collection_name=COLLECTION_NAME,
    query_vector=query_vector,
    limit=top_k,
    filter=metadata_filter,
    with_payload=True,
)

for point in search_result:
    print(f"Score: {point.score}, Title: {point.payload['title']}, Chunk: {point.payload['text'][:50]}...")

手持魔法探測器,Qdrant 自動指向最相關 chunk。

更多filter 請參考


4️⃣ Delete / 刪除 Points

論文撤回或版本過舊

from qdrant_client.http.models import Filter, FieldCondition, MatchValue

# 刪除指定 arxiv_id 的所有 chunks
delete_filter = Filter(
    must=[
        FieldCondition(key="arxiv_id", match=MatchValue(value=paper.arxiv_id))
    ]
)

qdrant_client.delete(collection_name=COLLECTION_NAME, filter=delete_filter)
print("Points deleted.")

刪除過期寶藏時,手起刀落,毫不手軟😎。


小結

今天,我學會了如何將寶藏安全存放,PostgreSQL 是結構化寶箱、Qdrant 是智慧地圖、MinIO 是可選倉庫。UPSERT 讓寶藏更新與新增輕鬆兼顧,批次上傳提升效率,Cosine similarity 幫助快速找到最相似的 chunk。傳統索引是「書籤 📖」,幫助快速翻頁;
向量索引是「磁場感應 🧲」,幫助快速找到相似的寶物。

回頭看,這些資料庫和向量庫就像我們的基地與魔法地圖,沒有它們,之前辛苦抓來的 PDF 寶藏可能就散落荒原,任由塵封。


上一篇
Day 4 | 征服第二個據點 — Arxiv Pipeline 技術拆解(下):PDF 向量化與 Qdrant 上傳
下一篇
Day 6|你好 Ollama - 與 Ollama 模型初次見面
系列文
論文流浪記:我與AI 探索工具、組合流程、挑戰完整平台9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言