Mục lục
Mục tiêu bài học
Sau bài này bạn sẽ:
- Hiểu Pinecone serverless hoạt động như thế nào và khi nào nên chọn nó.
- Tạo được index serverless với đúng
dimensionvàmetric. - Upsert, query với metadata filter, dùng namespace để tách dữ liệu.
- Update và delete vector theo ID hoặc theo metadata.
- Chạy được ví dụ RAG mini end-to-end với Pinecone + OpenAI embedding.
- Biết các giới hạn và trường hợp không phù hợp để dùng Pinecone.
Pinecone là gì
Pinecone là vector database dạng SaaS (Software as a Service) — không open source, không tự host. Bạn gọi API, Pinecone lo phần còn lại: hạ tầng, index, replica, scaling.
Hai chế độ triển khai
Từ 2024, Pinecone có 2 chế độ:
| Chế độ | Đặc điểm | Billing | Trạng thái |
|---|---|---|---|
| Serverless | Scale tự động, không pre-allocate. Lưu trữ tách riêng khỏi compute. | Pay-per-use (read/write/storage) | Recommended từ 2024 |
| Pod-based | Dedicated pod, pre-allocate capacity. Predictable latency hơn. | Giá theo pod type × số pod | Legacy, vẫn hỗ trợ |
Serverless phù hợp với phần lớn use case: không cần ước lượng capacity trước, trả tiền theo thực tế dùng. Pod-based còn dùng cho workload cần latency cực thấp và ổn định, hoặc khi cần các tính năng nâng cao chỉ có trên pod (ví dụ: delete theo filter — xem mục 9).
Free tier
Free tier cho phép 1 project, tối đa 5 index, capacity giới hạn (đủ cho hobby/POC với vài chục nghìn vectors). Không cần thẻ tín dụng để đăng ký.
Khi nào dùng Pinecone
- Production cần uptime cao và scaling mà không muốn tự vận hành Vector DB.
- Dataset từ vài trăm nghìn đến vài chục triệu vectors.
- Team nhỏ, không có người chuyên ops infra.
- Đang dùng AWS hoặc GCP — Pinecone serverless triển khai trên cùng cloud/region giúp giảm latency và egress cost.
Tạo account và lấy API key
- Truy cập
https://www.pinecone.iovà đăng ký (free tier, không cần thẻ). - Sau khi login, vào Dashboard → API Keys → Create API Key.
- Đặt tên key (ví dụ:
dev-key) và copy giá trị. - Lưu vào biến môi trường — không hard-code vào code:
# .env hoặc export trong shell
export PINECONE_API_KEY="pcsk_xxxxxxxxxxxxxxxxxxxxxxxx"
SDK Pinecone v5+ đọc key qua os.environ["PINECONE_API_KEY"]. Không dùng pattern cũ pinecone.init(api_key=..., environment=...) — đã deprecated từ SDK v3+ và bị xóa hoàn toàn ở v5+.
Cài SDK Python
pip install pinecone
# hoặc chỉ định major version để tránh breaking change
pip install "pinecone>=5,<6"
Package name từ SDK v3+ là pinecone (không phải pinecone-client như các version cũ). Kiểm tra version sau khi cài:
python -c "import pinecone; print(pinecone.__version__)"
# Mong đợi: 5.x.x
SDK v5+ import chính:
from pinecone import Pinecone— client chính.from pinecone import ServerlessSpec— spec cho serverless index.from pinecone import PodSpec— spec cho pod-based index (legacy).
Tạo index serverless
import os
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
# Tạo index (chỉ cần chạy 1 lần — idempotent nếu đã tồn tại)
INDEX_NAME = "ai-docs"
if INDEX_NAME not in pc.list_indexes().names():
pc.create_index(
name=INDEX_NAME,
dimension=1536, # phải khớp embedding model
metric="cosine", # cosine | euclidean | dotproduct
spec=ServerlessSpec(
cloud="aws",
region="us-east-1",
),
)
index = pc.Index(INDEX_NAME)
Dimension phải khớp embedding model
Dimension là số chiều của vector embedding. Phải đặt đúng khi tạo index — không thể thay đổi sau đó, muốn đổi phải xóa index và tạo lại.
| Embedding model | Dimension | Ghi chú |
|---|---|---|
OpenAI text-embedding-3-small |
1536 | Mặc định; có thể reduce xuống 512 với Matryoshka |
OpenAI text-embedding-3-large |
3072 | Có thể reduce xuống 1024, 256 |
OpenAI text-embedding-ada-002 |
1536 | Legacy, không thể reduce |
BAAI/bge-base-en-v1.5 |
768 | Open source, chạy local |
sentence-transformers/all-MiniLM-L6-v2 |
384 | Nhỏ gọn, nhanh, chất lượng vừa |
Metric
Metric cũng không đổi được sau khi tạo. Chọn dựa trên embedding model:
cosine— phù hợp với hầu hết text embedding (đo góc giữa 2 vector, bỏ qua magnitude).dotproduct— nếu model đã normalize vector thì tương đương cosine, nhưng nhanh hơn về compute. OpenAI khuyến nghịdotproductchotext-embedding-3-*.euclidean— ít dùng cho text embedding, phù hợp hơn cho feature vector không normalized.
Cloud và region
Serverless hỗ trợ AWS và GCP (2024). Chọn cloud/region gần nhất với nơi chạy backend để giảm latency. Với free tier, một số region bị hạn chế — xem trang docs chính thức để biết region nào available.
Upsert vectors
upsert là insert-or-update: nếu ID đã tồn tại thì ghi đè, nếu chưa thì thêm mới.
index = pc.Index(INDEX_NAME)
vectors = [
{
"id": "doc1#chunk0",
"values": [0.1, 0.2, ...], # list float, length = dimension
"metadata": {
"source": "doc1.pdf",
"page": 1,
"text": "Nội dung chunk đầu tiên của doc1...",
},
},
{
"id": "doc1#chunk1",
"values": [0.3, 0.1, ...],
"metadata": {
"source": "doc1.pdf",
"page": 2,
"text": "Nội dung chunk thứ hai...",
},
},
]
upsert_response = index.upsert(vectors=vectors)
print(upsert_response)
# UpsertResponse(upserted_count=2)
Tại sao lưu text vào metadata
Pinecone chỉ lưu vector và metadata — không lưu raw document. Để khi query lấy được nội dung gốc (dùng cho RAG), lưu text của chunk vào metadata["text"]. Metadata hỗ trợ kiểu dữ liệu: string, number, boolean, list of string.
Batch limit
Mỗi request upsert tối đa 100 vectors hoặc tổng payload 2MB. Nếu dataset lớn, tự chia batch:
def upsert_in_batches(index, vectors, batch_size=100):
"""Upsert vectors theo từng batch để tránh vượt giới hạn."""
for i in range(0, len(vectors), batch_size):
batch = vectors[i : i + batch_size]
index.upsert(vectors=batch)
print(f"Upserted {min(i + batch_size, len(vectors))}/{len(vectors)}")
SDK không tự chia batch — nếu truyền vào 200 vectors trong 1 lần gọi, SDK sẽ raise lỗi (PineconeApiException). Phải tự chia.
Query và metadata filter
query_vector = [0.05, 0.15, ...] # embedding của câu hỏi
result = index.query(
vector=query_vector,
top_k=5,
include_metadata=True,
include_values=False, # False mặc định, tiết kiệm bandwidth
filter={"source": {"$eq": "doc1.pdf"}},
)
for match in result.matches:
print(f"ID: {match.id}")
print(f"Score: {match.score:.4f}")
print(f"Text: {match.metadata['text'][:100]}")
print()
Response object:
result.matches— list các ScoredVector, đã sort theo score giảm dần.match.id— ID của vector.match.score— similarity score (cosine: 0 → 1, cao hơn = gần hơn).match.metadata— dict metadata đã lưu khi upsert (chỉ có khiinclude_metadata=True).match.values— vector gốc (chỉ có khiinclude_values=True).
Metadata filter operators
Filter syntax tương tự MongoDB:
| Operator | Ý nghĩa | Ví dụ |
|---|---|---|
$eq |
Bằng | {"source": {"$eq": "doc1.pdf"}} |
$ne |
Không bằng | {"source": {"$ne": "draft.pdf"}} |
$gt, $gte |
Lớn hơn (hoặc bằng) | {"page": {"$gte": 5}} |
$lt, $lte |
Nhỏ hơn (hoặc bằng) | {"page": {"$lt": 10}} |
$in |
Nằm trong danh sách | {"source": {"$in": ["doc1.pdf", "doc2.pdf"]}} |
$nin |
Không nằm trong danh sách | {"source": {"$nin": ["draft.pdf"]}} |
$and |
Tất cả điều kiện đúng | {"$and": [{"source": {"$eq": "doc1.pdf"}}, {"page": {"$gte": 2}}]} |
$or |
Ít nhất 1 điều kiện đúng | {"$or": [{"source": {"$eq": "doc1.pdf"}}, {"source": {"$eq": "doc2.pdf"}}]} |
Filter áp dụng trước khi tính similarity — chỉ các vector thỏa điều kiện mới được xét. Nếu filter quá nghiêm thì tập ứng viên nhỏ có thể ảnh hưởng đến recall. Khi tách hoàn toàn theo người dùng hoặc tenant, dùng namespace (xem mục 8) mạnh hơn filter.
Namespace — tách dữ liệu
Một index có thể chứa nhiều namespace. Mỗi namespace là một không gian tách biệt — upsert và query trong namespace A không ảnh hưởng và không nhìn thấy namespace B. Mặc định không chỉ định namespace thì dùng default namespace ("").
Use case phổ biến: 1 namespace cho mỗi tenant/user trong ứng dụng multi-tenant, hoặc 1 namespace cho mỗi "collection" tài liệu.
USER_NS = "user-123"
# Upsert vào namespace cụ thể
index.upsert(
vectors=[
{"id": "doc1#chunk0", "values": [...], "metadata": {"text": "..."}},
],
namespace=USER_NS,
)
# Query trong namespace đó
result = index.query(
vector=query_vector,
top_k=3,
include_metadata=True,
namespace=USER_NS,
)
Namespace tách cứng hơn metadata filter: query một namespace không bao giờ trả về kết quả từ namespace khác, kể cả khi bỏ filter. Namespace không tốn thêm chi phí tạo — chỉ tốn storage của vector trong đó.
Lưu ý: namespace không tự tạo declaration trước — upsert vào namespace nào thì namespace đó tự sinh ra. Delete toàn bộ vector trong namespace bằng delete_all=True:
index.delete(delete_all=True, namespace=USER_NS)
Update và Delete
Update metadata
index.update() cập nhật metadata của vector đã có (không thay đổi được values/vector sau khi upsert — muốn đổi vector phải upsert lại cùng ID):
# Cập nhật metadata của 1 vector
index.update(
id="doc1#chunk0",
set_metadata={"page": 2, "reviewed": True},
namespace="", # namespace mặc định
)
Delete theo ID
# Xóa 1 hoặc nhiều vector theo ID
index.delete(
ids=["doc1#chunk0", "doc1#chunk1"],
namespace="",
)
Delete toàn bộ namespace
index.delete(delete_all=True, namespace="user-123")
Delete theo filter (pod-based only)
Trên serverless, delete theo filter không được hỗ trợ — phải fetch ID trước rồi delete theo ID:
# Serverless: không có delete(filter=...)
# Workaround: query → lấy ID → delete theo ID
# Tìm vector để delete (query bằng zero vector để lấy theo filter)
# Lưu ý: cách này có giới hạn về số vector trả về
result = index.query(
vector=[0.0] * 1536,
top_k=1000,
include_metadata=False,
filter={"source": {"$eq": "old_doc.pdf"}},
)
ids_to_delete = [match.id for match in result.matches]
if ids_to_delete:
index.delete(ids=ids_to_delete)
Trên pod-based, index.delete(filter={...}) hoạt động trực tiếp không cần workaround này.
Index stats
stats = index.describe_index_stats()
print(stats)
Output mẫu:
DescribeIndexStatsResponse(
dimension=1536,
index_fullness=0.0,
namespaces={
"": NamespaceSummary(vector_count=150),
"user-123": NamespaceSummary(vector_count=42),
},
total_vector_count=192,
)
Trường hữu ích:
total_vector_count— tổng số vector trong toàn bộ index (mọi namespace).namespaces— dict namespace → số vector. Dùng để kiểm tra dữ liệu đã upsert đúng chưa.index_fullness— chỉ có nghĩa với pod-based; serverless luôn trả về 0.
Sau khi upsert, stats có thể chưa cập nhật ngay lập tức — có delay vài giây đến vài chục giây trên serverless trước khi vector count thay đổi.
RAG mini end-to-end
Ví dụ dưới đây embed 5 chunk văn bản bằng OpenAI text-embedding-3-small, upsert vào Pinecone, sau đó query 1 câu hỏi và in top-3 kết quả.
import os
from openai import OpenAI
from pinecone import Pinecone, ServerlessSpec
# --- Config ---
PINECONE_API_KEY = os.environ["PINECONE_API_KEY"]
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
INDEX_NAME = "rag-demo"
EMBED_MODEL = "text-embedding-3-small"
DIMENSION = 1536
# --- Init clients ---
pc = Pinecone(api_key=PINECONE_API_KEY)
openai_client = OpenAI(api_key=OPENAI_API_KEY)
# --- Tạo index nếu chưa có ---
if INDEX_NAME not in pc.list_indexes().names():
pc.create_index(
name=INDEX_NAME,
dimension=DIMENSION,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1"),
)
index = pc.Index(INDEX_NAME)
# --- Dữ liệu mẫu: 5 chunks ---
chunks = [
{
"id": "c0",
"text": "FastAPI là framework Python hiệu suất cao cho xây dựng API.",
"source": "web-framework.txt",
},
{
"id": "c1",
"text": "Pydantic dùng Python type hints để validate dữ liệu tự động.",
"source": "web-framework.txt",
},
{
"id": "c2",
"text": "Vector database lưu embedding dạng float array và hỗ trợ tìm kiếm ANN.",
"source": "vector-db.txt",
},
{
"id": "c3",
"text": "HNSW là thuật toán index phổ biến nhất trong vector database vì tốc độ và recall tốt.",
"source": "vector-db.txt",
},
{
"id": "c4",
"text": "RAG kết hợp retrieval từ knowledge base với generation của LLM để trả lời chính xác hơn.",
"source": "rag-overview.txt",
},
]
# --- Embed tất cả chunks ---
texts = [c["text"] for c in chunks]
embed_response = openai_client.embeddings.create(model=EMBED_MODEL, input=texts)
embeddings = [item.embedding for item in embed_response.data]
# --- Upsert vào Pinecone ---
vectors = [
{
"id": c["id"],
"values": emb,
"metadata": {"text": c["text"], "source": c["source"]},
}
for c, emb in zip(chunks, embeddings)
]
index.upsert(vectors=vectors)
print("Upserted", len(vectors), "vectors")
# --- Query ---
question = "Vector database dùng thuật toán gì để tìm kiếm?"
q_embed = openai_client.embeddings.create(model=EMBED_MODEL, input=[question])
q_vector = q_embed.data[0].embedding
results = index.query(
vector=q_vector,
top_k=3,
include_metadata=True,
)
print(f"\nQuery: {question}\n")
for i, match in enumerate(results.matches, 1):
print(f"[{i}] score={match.score:.4f} | {match.metadata['text']}")
Output mẫu:
Upserted 5 vectors
Query: Vector database dùng thuật toán gì để tìm kiếm?
[1] score=0.8821 | HNSW là thuật toán index phổ biến nhất trong vector database vì tốc độ và recall tốt.
[2] score=0.7634 | Vector database lưu embedding dạng float array và hỗ trợ tìm kiếm ANN.
[3] score=0.5412 | RAG kết hợp retrieval từ knowledge base với generation của LLM để trả lời chính xác hơn.
Chunk liên quan nhất (HNSW) có score cao nhất. Với RAG thực tế, bước tiếp theo là đưa top-k chunks vào context của LLM và gọi chat.completions.create() để sinh câu trả lời cuối.
Lưu ý về stats delay
Nếu chạy query ngay sau upsert và không thấy kết quả, chờ vài giây rồi thử lại. Pinecone serverless có delay nhỏ giữa upsert và lúc vector visible cho query.
Cost model serverless
Pinecone serverless tính phí trên 3 chiều: reads (query), writes (upsert), và storage. Pinecone thay đổi pricing định kỳ — số liệu cụ thể có thể lỗi thời, luôn kiểm tra trang pinecone.io/pricing trước khi lập kế hoạch chi phí.
Cấu trúc chi phí cần hiểu:
- Read units — mỗi query tốn 1 hoặc nhiều read unit tùy
top_kvà số vector trong index. Query vớitop_k=10trên index nhỏ rẻ hơn index lớn. - Write units — mỗi vector upsert tốn 1 write unit (tính theo số vector, không phải kích thước batch).
- Storage — tính theo tổng storage tháng (GB·month). Storage phụ thuộc dimension: 1536-dim float32 = 6KB/vector; 1 triệu vectors ≈ 6GB.
Ước lượng chi phí thực tế:
- POC / prototype với vài chục nghìn vectors và ít query: free tier đủ dùng.
- Dataset 1 triệu vectors, 10.000 query/ngày: cần ước lượng trên pricing calculator của Pinecone.
- Khi scale lên vài chục triệu vectors, chi phí storage là yếu tố chính — cân nhắc reduce dimension (nếu model hỗ trợ Matryoshka) hoặc chuyển sang tự host.
Free tier không yêu cầu thẻ tín dụng và phù hợp cho giai đoạn development. Khi chuyển sang production, bật billing alert trên dashboard để tránh chi phí không mong muốn.
Khi nào không dùng Pinecone
- Dataset nhỏ (< 100k vectors): một local vector DB như ChromaDB chạy in-process đơn giản hơn và không tốn tiền.
- Cần on-premise hoặc data sovereignty: dữ liệu không được ra khỏi hạ tầng nội bộ — dùng Qdrant hoặc Weaviate tự host. Pinecone là SaaS nên dữ liệu nằm trên server của họ.
- Budget eo hẹp ở scale vừa: khi dataset đủ lớn để chi phí serverless đáng kể, so sánh với chi phí tự host Qdrant trên 1 VM nhỏ.
- Cần delete theo filter trên serverless: tính năng này không có — phải workaround (query ID rồi delete). Nếu cần xóa bulk theo điều kiện thường xuyên, pod-based hoặc self-host Qdrant/Weaviate phù hợp hơn.
- Cần customize index parameters nâng cao: Pinecone ẩn hoàn toàn cấu hình HNSW (ef, M, efConstruction). Nếu cần tune index params, tự host sẽ linh hoạt hơn.
Common pitfalls
1. Tạo index sai dimension
Dimension không thay đổi được sau khi tạo. Nếu upsert vector có length khác dimension của index, SDK raise lỗi ngay lập tức. Muốn sửa: xóa index (pc.delete_index(name)) và tạo lại với dimension đúng. Kiểm tra kỹ trước khi tạo index trên production.
2. Upsert vượt batch limit
Truyền hơn 100 vectors trong 1 lần upsert() sẽ báo lỗi. Luôn chia batch khi insert bulk — dùng helper như hàm upsert_in_batches() ở mục 6.
3. Quên include_metadata=True
Mặc định query không trả về metadata. Khi viết code RAG mà thấy match.metadata là None hoặc {}, kiểm tra xem đã truyền include_metadata=True chưa.
4. Dùng old SDK pattern
Pattern cũ không hoạt động với SDK v5+:
# SAI — deprecated từ v3, bị xóa hoàn toàn ở v5+
import pinecone
pinecone.init(api_key="...", environment="us-east1-gcp")
index = pinecone.Index("my-index")
Pattern đúng với SDK v5+:
# ĐÚNG
from pinecone import Pinecone
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
index = pc.Index("my-index")
5. Cold start latency trên serverless
Index serverless sau một thời gian không có request có thể chuyển sang trạng thái idle. Query đầu tiên sau khi idle sẽ chậm hơn vài trăm ms đến vài giây (cold start). Trong production, có thể gửi periodic "ping query" để giữ index ấm — đánh đổi một lượng nhỏ read unit.
6. Metadata value kiểu list
Pinecone hỗ trợ metadata value là list of string (không phải list of int hay nested object). Nếu lưu list số vào metadata, sẽ bị reject hoặc convert không đúng. Serialize thành string trước khi lưu nếu cần.
Bài tiếp theo
Bài 16: Qdrant và Weaviate — alternatives open source — Hai lựa chọn self-host phổ biến khi cần kiểm soát hạ tầng hoặc tối ưu chi phí ở scale lớn.
