索引:嵌入并存储分块
使用 OpenAI 嵌入 API 为每个分块生成嵌入,并将生成的带元数据向量写入向量存储,构建可搜索的文档索引。
索引:嵌入并存储分块 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
The Indexing Stage: Overview
After loading and chunking your documents, you reach the indexing stage: converting text chunks into vector embeddings and storing them in a searchable vector database. This is the final offline step before queries can be answered. The quality of your embeddings and the efficiency of your storage and indexing strategy directly determine how fast and accurate your RAG system will be at query time.
Generating Embeddings via OpenAI API
The most common approach is to call OpenAI's embeddings API with your chunk text. The text-embedding-3-small model produces 1536-dimensional vectors and costs $0.02 per million tokens — extremely cheap for most workloads. Send multiple texts in a single API call (up to 2048 inputs) to maximize throughput. The response contains one embedding vector per input text in the same order.
from openai import OpenAI
client = OpenAI()
def embed_batch(texts, model='text-embedding-3-small'):
response = client.embeddings.create(
model=model,
input=texts # up to 2048 texts per call
)
return [item.embedding for item in response.data]
# Embed one batch of 100 chunk texts
texts = [chunk['text'] for chunk in chunks[:100]]
vectors = embed_batch(texts)
print(f'Embedding dimension: {len(vectors[0])}')
print(f'Embedded {len(vectors)} chunks')Batching for Efficiency
When indexing thousands of chunks, efficiency matters. Process chunks in batches of 100-500 to balance throughput and memory usage. Track your position so you can resume after a failure without re-embedding already processed chunks. Log progress regularly. For 100,000 chunks at 500 per batch, you will make 200 API calls — this typically completes in a few minutes.
def embed_all_chunks(chunks, batch_size=200):
embedded = []
total = len(chunks)
for i in range(0, total, batch_size):
batch = chunks[i:i+batch_size]
texts = [c['text'] for c in batch]
vectors = embed_batch(texts)
for chunk, vector in zip(batch, vectors):
embedded.append({
**chunk,
'embedding': vector
})
if (i // batch_size) % 10 == 0:
print(f'Progress: {min(i+batch_size, total)}/{total}')
return embeddedRate Limit Handling During Indexing
The OpenAI embeddings API has rate limits measured in tokens per minute (TPM). Large indexing jobs hit these limits and receive RateLimitError. Implement exponential backoff with jitter: when a rate limit error occurs, wait a brief random interval before retrying, doubling the wait on each subsequent failure. This spreads retries out and prevents all parallel workers from hammering the API at the same moment.
import time
import random
from openai import RateLimitError
def embed_batch_with_retry(texts, max_retries=5):
for attempt in range(max_retries):
try:
return embed_batch(texts)
except RateLimitError:
if attempt == max_retries - 1:
raise
wait = (2 ** attempt) + random.uniform(0, 1)
print(f'Rate limited. Waiting {wait:.1f}s...')
time.sleep(wait)
return []Upserting Vectors into Pinecone
After generating embeddings, upsert them into the vector store. Upserting means inserting new vectors or updating existing ones if the same ID already exists — idempotent by design. In Pinecone, each upserted record contains the vector ID, the embedding values, and a metadata dictionary of fields you want to filter or display later. Upsert in batches of up to 100 records per call for optimal throughput.
import pinecone
pc = pinecone.Pinecone(api_key='YOUR_KEY')
index = pc.Index('rag-index')
def upsert_to_pinecone(embedded_chunks, batch_size=100):
for i in range(0, len(embedded_chunks), batch_size):
batch = embedded_chunks[i:i+batch_size]
vectors = [
(
chunk['id'],
chunk['embedding'],
{
'text': chunk['text'],
'source': chunk['metadata']['source'],
'page': chunk['metadata'].get('page', 0)
}
)
for chunk in batch
]
index.upsert(vectors=vectors)
print(f'Upserted {min(i+batch_size, len(embedded_chunks))}/{len(embedded_chunks)}')Storing in pgvector
With pgvector, you insert embeddings directly into a PostgreSQL table using standard SQL. The vector data type accepts a Python list of floats serialized as a string. After inserting all rows, create an HNSW index for fast approximate nearest neighbor queries. Indexing an existing table with millions of rows can take several minutes, so build the index after bulk insertion rather than before.
import psycopg2
from psycopg2.extras import execute_values
def upsert_to_pgvector(conn, embedded_chunks):
with conn.cursor() as cur:
records = [
(
chunk['id'],
chunk['text'],
chunk['metadata']['source'],
chunk['metadata'].get('page', 0),
chunk['embedding'] # list of floats
)
for chunk in embedded_chunks
]
execute_values(cur, '''
INSERT INTO document_chunks (id, text, source, page, embedding)
VALUES %s
ON CONFLICT (id) DO UPDATE
SET text = EXCLUDED.text, embedding = EXCLUDED.embedding
''', records)
conn.commit()Building the HNSW Index
HNSW (Hierarchical Navigable Small World) is the index type that enables fast approximate nearest neighbor search. Unlike brute-force search (which compares the query vector against every stored vector), HNSW builds a multi-layer graph structure that prunes the search space. The m parameter controls how many connections each node has (higher = better recall but more memory), and ef_construction controls index quality during build time.
-- Build HNSW index after bulk insertion
CREATE INDEX CONCURRENTLY ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Set ef_search at query time to trade recall vs speed
SET hnsw.ef_search = 100;
-- Verify index was created
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'document_chunks';Metadata Schema Design
Metadata stored alongside each vector enables powerful filtered retrieval. Design your metadata schema before indexing — adding new fields later requires re-indexing. Include fields you will filter on (department, doc_type, date range), fields you will display in citations (title, page, author), and fields useful for debugging (chunk_index, total_chunks, indexed_at). Keep metadata values simple: strings, numbers, and booleans index and filter efficiently; nested objects do not.
# Well-designed metadata schema
METADATA_SCHEMA = {
# For filtering at retrieval time
'department': 'HR', # string
'doc_type': 'policy', # string
'year': 2025, # integer
'is_active': True, # boolean
# For display in citations
'title': 'Employee Handbook 2025',
'author': 'HR Team',
'page': 12,
'source': 's3://docs/handbook_2025.pdf',
# For debugging and updates
'chunk_index': 3,
'total_chunks': 24,
'indexed_at': '2025-09-01T10:00:00Z'
}Checkpointing Long Indexing Jobs
Indexing a large corpus can take hours. A crash midway wastes all progress. Implement a checkpoint file that records which chunks have been successfully indexed. On restart, skip already-indexed chunks and continue from where you left off. This makes the indexing job idempotent and safe to resume. Store the checkpoint as a set of processed chunk IDs in a JSON file or database table.
import json
from pathlib import Path
CHECKPOINT_FILE = '/tmp/index_checkpoint.json'
def load_checkpoint():
if Path(CHECKPOINT_FILE).exists():
return set(json.loads(Path(CHECKPOINT_FILE).read_text()))
return set()
def save_checkpoint(indexed_ids):
Path(CHECKPOINT_FILE).write_text(json.dumps(list(indexed_ids)))
def index_with_checkpoint(chunks, index):
done = load_checkpoint()
remaining = [c for c in chunks if c['id'] not in done]
print(f'Resuming: {len(done)} done, {len(remaining)} remaining')
for chunk in remaining:
upsert_to_pinecone([chunk], index)
done.add(chunk['id'])
save_checkpoint(done)Verifying Index Completeness
After indexing, verify that all chunks made it into the vector store. Compare the number of chunks produced by your splitter against the vector count reported by the index. Query the index with a known document's text and confirm that the expected result appears in the top 5. Run a few known queries from your golden test set and check that precision is at the expected level. Never assume the index is complete without verifying it.
def verify_index(index, chunks, sample_size=10):
index_stats = index.describe_index_stats()
total_vectors = index_stats.total_vector_count
expected = len(chunks)
print(f'Index vectors: {total_vectors}, Expected: {expected}')
if total_vectors != expected:
print('WARNING: mismatch — some chunks may not have been indexed')
# Spot-check retrieval
import random
sample = random.sample(chunks, sample_size)
for chunk in sample:
vec = embed_batch([chunk['text']])[0]
results = index.query(vector=vec, top_k=1, include_metadata=True)
top_id = results.matches[0].id if results.matches else None
if top_id != chunk['id']:
print(f'WARNING: expected {chunk["id"]}, got {top_id}')Local Embedding Alternatives
For privacy-sensitive data that cannot leave your infrastructure, use locally hosted embedding models. The sentence-transformers library provides high-quality models like all-MiniLM-L6-v2 (384-dim, 22MB, very fast) and bge-large-en-v1.5 (1024-dim, better quality). Run them on CPU for moderate workloads or GPU for large indexing jobs. Local models eliminate API costs and data egress but require managing model files and compute resources.
from sentence_transformers import SentenceTransformer
# Load once at startup
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
def embed_locally(texts, batch_size=64):
# encode() handles batching internally
embeddings = model.encode(
texts,
batch_size=batch_size,
show_progress_bar=True,
convert_to_numpy=True
)
return embeddings.tolist() # convert numpy array to Python list
vectors = embed_locally([c['text'] for c in chunks])Quick Check
Test your understanding of AI Engineering concepts from this lesson.
Lesson Recap
In this lesson you learned: generating embeddings in batches with the OpenAI API and handling rate limits with exponential backoff, upserting vectors into Pinecone and pgvector with metadata, building HNSW indexes for fast approximate nearest neighbor search, and production best practices including checkpoint-based resumption, metadata schema design, and index completeness verification. Next up we build the query pipeline that retrieves chunks and generates grounded answers.
常见问题解答
「索引:嵌入并存储分块」课时是免费的吗?
是的 — 「索引:嵌入并存储分块」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。
「索引:嵌入并存储分块」这节课中我会学到什么?
使用 OpenAI 嵌入 API 为每个分块生成嵌入,并将生成的带元数据向量写入向量存储,构建可搜索的文档索引。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 AI Engineering Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「索引:嵌入并存储分块」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 AI Engineering Academy 课中编写并运行代码吗?
能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 文档加载与文本提取
- 分块策略:固定大小、按句子与递归
- 索引:嵌入并存储分块
- 查询、检索与生成