今天,我的冒險船再次啟航,目標是將前幾天辛苦收集的 arXiv 論文寶藏,安全地存放進兩座寶庫——PostgreSQL 與 Qdrant。這就像探險者在遼闊的荒原上,不只要找到寶藏,還得把它妥善分類、標記,才能在日後快速取出使用。上一回我們只是抓到寶藏與打開 PDF,今天則要學會「編號、上鎖、地圖標記」的完整流程。
前方的挑戰明顯:process_pdfs_task
將資料送往 PostgreSQL 或 MinIO(可選),而 qdrant_index_task
將文字 chunk 轉向量後存入 Qdrant。雖然之前只簡單提過,今天我將揭開這三個工具的神秘面紗,以及我為什麼選它們。
PostgreSQL 管 metadata,Qdrant 管語意檢索,MinIO 管檔案備份。三者合力,才構成完整的知識寶庫。
小提醒:安裝完 PostgreSQL 和 Qdrant 後,就可以打開 Qdrant Dashboard (http://localhost:6333/dashboard) 欣賞它精美的 UI,並搭配 DBeaver 觀察 PostgreSQL。
之後會再說明安裝細節。
在資料工程世界,每一篇論文都是一個珍貴寶物。
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 | 原始檔案儲存 | 防水袋 💧 |
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 處理狀態都標示清楚,方便快速檢索。
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 的位置唯一,方便未來查找或更新。
WHERE
、JOIN
等條件匹配目的:加速「相似度搜尋」(Approximate Nearest Neighbor, ANN)
流程:
upsert
)特性:
CREATE INDEX
(除非要調參或換演算法)在 Qdrant 這種向量資料庫裡:
upsert()
→ 資料進 collection → 系統自動放進 HNSW 索引search()
→ 就是走這個 HNSW graph換句話說:在 SQL 裡「索引是額外的結構」;在向量 DB 裡「索引就是存放資料的方式」。
連線 Qdrant
from qdrant_client import QdrantClient, models
from config import COLLECTION_NAME, QDRANT_URL
qdrant_client = QdrantClient(url=QDRANT_URL, timeout=300)
初始化資料集
# 建立 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
每次新論文進來或更新時
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 的組合:
UPSERT = INSERT + UPDATE
沒寶藏 → 放新寶物
已有寶藏 → 補充或替換
檢索語意相關 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 請參考
論文撤回或版本過舊
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 寶藏可能就散落荒原,任由塵封。