# Day 2. [PPT Chatbot] Qdrant + BM25 介紹
## 0. 前言
第一次碰到向量資料庫,只好google找找資料,剛好鐵人賽有人做整理,真的是天降甘霖。
[Day18 GAI爆炸時代 - 市面上 Vector DB 比較](https://ithelp.ithome.com.tw/articles/10344993)

這張圖對剛入門的我真的是很有幫助。
首先,commercial的先跳過,不想在測試的時候遇到需要商業版才能用的功能。
再來,傳統database (row-based/column-based) 的先跳過,不想花時間調整參數優化,也沒有多的硬體可以使用。
最後,目標就放在左上角的區塊。Qdrant在作者的評比中,缺點對我來說比較還好。且官方document也寫得很清楚,感覺學習曲線不高,所以就決定是Qdrant了。
## 1. Qdrant
### 1-1. Qdrant安裝
非常簡單,參考[Local Qdrant Quick Start](https://qdrant.tech/documentation/quickstart/),用docker架起來,大概5分鐘就可以搞定。只是在Windows環境要額外設定Volume的位置,以防資料不見。
### 1-2. QDrant儲存概念

[圖片來源] [What is Qdrant?](https://qdrant.tech/documentation/overview/)
Qdrant的儲存名詞
- point:由id、vector和payload組成,可以想像成是切過的chunk。
- id:id
- vector:轉換後的高維度向量
- payload:就是metadata,json格式
- collection:多個point組成的集合
- High-Level Overview of Qdrant’s Architecture

[圖片來源] [What is Qdrant?](https://qdrant.tech/documentation/overview/)
### 1-3. 新增資料到Qdrant
這邊用ChatGPT產生的example code來解釋。
``` python
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# Step 1: Connect to Qdrant (local or cloud)
client = QdrantClient(host="localhost", port=6333) # Replace with your cloud endpoint if needed
# Step 2: Create a collection
collection_name = "demo_collection"
client.recreate_collection(
collection_name=collection_name,
vectors_config=VectorParams(size=4, distance=Distance.COSINE)
)
# Step 3: Insert points (vectors + payloads)
points = [
PointStruct(
id=1,
vector=[0.1, 0.2, 0.3, 0.4],
payload={"category": "tech", "title": "Intro to AI"}
),
PointStruct(
id=2,
vector=[0.2, 0.1, 0.4, 0.3],
payload={"category": "health", "title": "Wellness Tips"}
)
]
client.upsert(collection_name=collection_name, points=points)
print(f"Inserted {len(points)} points into '{collection_name}'")
```
- Step 1:
- 連線到Qdrant預設的port 6333。
- Step 2:
- 建立collection
- 可以設定vectors_config
- size:設定向量有多少維度。
- distance:向量化的距離演算法,預設是consine。
- Step 3:
- 建立Points。
- ID:建議用可以重現的ID,以便可以用upsert時可以更新資料。
- vector:向量化後的數值陣列。
- payload:給這個Ponint一些標籤,也可以理解成hash tag。
- 透過upsert方法將Point陣列新增到Qdrant
- upsert:跟SQL的概念一樣,會先確認是否有同個id,若有就更新,沒有就新增。
### 1-4. 查詢Qdrant
這邊用ChatGPT產生的example code來解釋。
```python
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue
# Connect to Qdrant
client = QdrantClient(host="localhost", port=6333)
# Define a query vector (same dimension as collection)
query_vector = [0.15, 0.25, 0.35, 0.45]
# Optional: filter by payload (e.g., category = "tech")
search_filter = Filter(
must=[
FieldCondition(
key="category",
match=MatchValue(value="tech")
)
]
)
# Perform the search
search_results = client.query_points(
collection_name="demo_collection",
query_vector=query_vector,
limit=5,
query_filter=search_filter # Optional
)
# Display results
for result in search_results:
print(f"ID: {result.id}, Score: {result.score}, Payload: {result.payload}")
```
- query_vector:就是user prompt轉換成向量的樣子
- search_filter (optional):先搜尋payload (metadata),找出符合的Point (chunk)。
- search:從指定的collection中,先用payload篩選Point,再用向量相似(vector similarity)搜尋的相近內容,並選出三個。
## 2. BM25演算法
> BM25 (Best Matching 25) 是一種用於資訊檢索的經典排序演算法,用於計算文件與使用者搜尋查詢之間的相關性分數。
> 向量相似度(Dense Search):擅長語義相似,即使文件和查詢使用同義詞也能匹配(例如,查詢 "狗" 可以找到包含 "犬" 的文件)。
> 稀疏向量搜尋(Sparse Vector Search),而 BM25 就是最常用、最經典的稀疏向量演算法。擅長關鍵字匹配,基於詞彙出現頻率(Term Frequency, TF)和逆文件頻率(Inverse Document Frequency, IDF)計算分數。
### 2-1. limit參數 (Top K)
在1-4的範例中,有個limit參數,表示回傳多少個「向量相似」的內容。
這會出現一種情況 - 「無論哪個Point,都可以算出與user prompt語意上的近似度,都會回傳滿足limit設定數量的Point」。
範例:
- limit:5 (但實際有關的只有2個Point)
- user prompt:什麼是BM25?
- 可能的結果:
|Score | Point |
| -------- | -------- |
| 0.77754 | BM25是一種用於資訊檢索的經典排序演算法 |
| 0.75524 | BM25參考了 TF-IDF 演算法並進行了改進 |
| 0.74512 | BMW是來自德國的高級汽車品牌 |
| 0.72315 | 我今天晚餐想吃麥當勞 |
| 0.71023 | 哥開的不是車,是速度 |
暗黑大大的文章也提到這種情況 - [Qdrant 向量資料庫基本練習](https://blog.darkthread.net/blog/qdrant-w-cs/)。
單純的向量相似度搜尋可能會將語義相關但內容不夠精確的文件排在高位,
所以這時候就需要再套一層演算法去確保關鍵詞的精準匹配。
## 2-2. Hybrid Search
> BM25 的實作是為了通過 Hybrid Search (混合搜尋) 來解決純向量搜尋的盲點,平衡語義相關性和關鍵字精確性,使您的 RAG 應用能夠提取更準確的上下文。
這邊用ChatGPT產生的example code來解釋。
```python
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue
from rank_bm25 import BM25Okapi
import numpy as np
# Step 1: Connect to Qdrant
client = QdrantClient(host="localhost", port=6333)
# Step 2: Define query vector and retrieve top-k points
query_vector = [0.15, 0.25, 0.35, 0.45]
top_k = 10
results = client.query_points(
collection_name="demo_collection",
query_vector=query_vector,
limit=top_k
)
# Step 3: Extract text payloads
documents = []
id_map = []
for point in results:
text = point.payload.get("text", "")
if text:
documents.append(text.split()) # Tokenize
id_map.append((point.id, text))
# Step 4: Apply BM25
bm25 = BM25Okapi(documents)
query_text = "AI introduction and applications"
tokenized_query = query_text.split()
bm25_scores = bm25.get_scores(tokenized_query)
# Step 5: Combine vector score and BM25 score
combined = []
for i, point in enumerate(results):
bm25_score = bm25_scores[i]
vector_score = point.score
combined_score = 0.5 * vector_score + 0.5 * bm25_score # Weighted blend
combined.append((point.id, combined_score, point.payload))
# Step 6: Sort and display
combined.sort(key=lambda x: x[1], reverse=True)
for pid, score, payload in combined:
print(f"ID: {pid}, Combined Score: {score:.4f}, Text: {payload.get('text')}")
```
- Step 1~3都一樣,找出相關的Points
- Step 4:
- tokenized_query:將user prompt分詞化([tokenize](https://ithelp.ithome.com.tw/articles/10385077))。
- bm25_score:將Point內容和分詞後的user prompt計算分數。
- Step 5:
- 結合向量分數和BM25分數,並根據權重算出最後的分數。
重新計算分數的結果:
|Score | Point |
| -------- | -------- |
| 0.98551 | BM25是一種用於資訊檢索的經典排序演算法 |
| 0.81614 | BM25參考了 TF-IDF 演算法並進行了改進 |
| 0 | BMW是來自德國的高級汽車品牌 |
| 0 | 我今天晚餐想吃麥當勞 |
| 0 | 哥開的不是車,是速度 |
在Step 5也可以設定Top N來表示取重新計算後的前幾名。
如果這時候N = 1,就會取得第一筆「BM25是一種用於資訊檢索的經典排序演算法」,那第2筆就不會被丟出來了。
## 3. 小結
今天的文章介紹怎麼使用Qdrant新增資料和查詢資料,但查詢時可以設定Top K,只回傳指定數量的Point。然而會遇到「分數都很高,但怎麼會參雜一些無關的point」,此時可以運用BM25演算法,重新計算point的分數。此時也可以設定Top N來表示取重新計算後的前幾名,所以設定參數的策略也是門學問。