vector-db-ingestion
Skill0 starsChanged 7 months agoMITView on GitHub
Build a bulk data ingestion pipeline for vector databases (pgvector, Pinecone, Qdrant, etc.) with batched embeddings, schema validation, deduplication, and error handling. Use when building RAG knowledge bases, semantic search systems, or any application that needs to embed and store text chunks at scale.
In this repository37 other files it gives its agents.
- ablation-study-frameworkSkill
- async-http-websocket-patternsSkill
- brain-dumpSkill
- chaos-controlled-behavior-systemSkill
- daily-digestSkill
- database-reset-and-seedSkill
- duckdb-analytics-layerSkill
- event-driven-backtest-engineSkill
- event-study-full-pipelineSkill
- event-study-stats-suiteSkill
- failure-learning-systemSkill
- fastapi-sse-live-dashboardSkill
- goal-task-decompositionSkill
- high-information-gain-interviewerSkill
- idea-extraction-batch-pipelineSkill
- idea-grouping-taxonomySkill
- interview-belief-extractorSkill
- journal-captureSkill
- journal-reviewSkill
- keyword-filter-frameworkSkill
- langgraph-agentic-pipelineSkill
- llm-as-judge-eval-frameworkSkill
- multi-tab-dashboard-rendererSkill
- multi-tier-memory-systemSkill
- parquet-storage-with-walSkill
- project-insightsSkill
- pydantic-typed-config-loaderSkill
- quality-trend-detectionSkill
- research-gap-finderSkill
- safe-commitSkill
- simtom-knowledge-isolationSkill
- statistical-signal-detectionSkill
- streaming-dataset-pipelineSkill
- structured-data-extraction-pipelineSkill
- structured-logging-setupSkill
- system-health-monitoringSkill
- textual-tui-reactiveSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 3 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 41
Every chunk needs a consistent schema.
- Distinctive: line 78
Always embed in batches to respect rate limits and maximize throughput.
- Distinctive: line 120
Before inserting, check if a near-duplicate already exists:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Vector DB Ingestion Pipeline
Ingests text chunks into a vector database with validation, batched embedding generation, cosine-similarity deduplication, and atomic insertion. Handles both single-record ingestion and bulk loading.
Architecture Overview
Source Data (JSON files, APIs, docs)
|
v
[1. Load] — Read chunks from batch files
|
v
[2. Validate] — Schema checks, required fields, token estimation
|
v
[3. Embed] — Batched embedding generation (100 at a time)
|
v
[4. Dedup] — Cosine similarity check against existing vectors
|
v
[5. Insert] — Upsert into vector DB with ON CONFLICT handling
|
v
[6. Verify] — Count check, domain breakdownStep 1: Define the Chunk Schema and Validation
Every chunk needs a consistent schema. Validate before embedding to avoid wasting API calls:
from dataclasses import dataclass
@dataclass
class ValidationResult:
valid: bool
errors: list[str]
REQUIRED_FIELDS = ["domain", "topic", "content", "summary", "source_citation",
"source_tier", "evidence_strength"]
def validate_chunk(chunk: dict) -> ValidationResult:
"""Validate a chunk has all required fields and passes quality checks."""
errors = []
for field in REQUIRED_FIELDS:
if not chunk.get(field):
errors.append(f"Missing required field: {field}")
content = chunk.get("content", "")
if len(content) < 50:
errors.append("Content too short (< 50 chars)")
if len(content) > 10000:
errors.append("Content too long (> 10000 chars)")
return ValidationResult(valid=len(errors) == 0, errors=errors)
def estimate_tokens(text: str) -> int:
"""Rough token count estimate (~4 chars per token)."""
return len(text) // 4Step 2: Embedding Generation (Batched)
Always embed in batches to respect rate limits and maximize throughput. Use summary + content for richer embeddings:
import time
def embed_texts(texts: list[str], model="text-embedding-3-small") -> list[list[float]]:
"""Embed a batch of texts. Wraps your embedding provider."""
from openai import OpenAI
client = OpenAI()
response = client.embeddings.create(input=texts, model=model)
return [item.embedding for item in response.data]
def embed_text(text: str) -> list[float]:
"""Embed a single text."""
return embed_texts([text])[0]
def batch_embed(chunks: list[dict], batch_size: int = 100) -> list[list[float]]:
"""Embed all chunks in batches with progress reporting."""
all_embeddings = []
t0 = time.time()
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i + batch_size]
# Combine summary + content for richer embedding
texts = [f"{c['summary']}\n\n{c['content']}" for c in batch]
embs = embed_texts(texts)
all_embeddings.extend(embs)
elapsed = time.time() - t0
done = i + len(batch)
rate = done / elapsed
print(f" {done}/{len(chunks)} embedded ({elapsed:.0f}s, {rate:.1f}/s)")
time.sleep(0.2) # rate limit buffer
return all_embeddingsStep 3: Deduplication via Cosine Similarity
Before inserting, check if a near-duplicate already exists:
def find_near_duplicates(
embedding: list[float],
domain: str,
threshold: float = 0.95,
db_conn=None,
) -> list[dict]:
"""Find existing chunks with cosine similarity above threshold."""
vec_str = f"[{','.join(str(v) for v in embedding)}]"
with db_conn.cursor() as c:
c.execute(
"""
SELECT id, topic,
1 - (embedding <=> %s::vector) AS cosine_sim
FROM knowledge_chunks
WHERE domain = %s
AND 1 - (embedding <=> %s::vector) > %s
ORDER BY cosine_sim DESC
LIMIT 3
""",
(vec_str, domain, vec_str, threshold),
)
return [
{"id": str(row[0]), "topic": row[1], "cosine_sim": float(row[2])}
for row in c.fetchall()
]Step 4: Single-Record Ingestion (with Dedup)
For real-time ingestion (e.g., adding one chunk at a time):
def ingest_chunk(chunk: dict, skip_dedup: bool = False, db_conn=None) -> dict:
# Validate
qa = validate_chunk(chunk)
if not qa.valid:
return {"status": "rejected", "errors": qa.errors}
# Embed
embed_input = f"{chunk['summary']}\n\n{chunk['content']}"
try:
embedding = embed_text(embed_input)
except Exception as e:
return {"status": "error", "errors": [f"embedding failed: {e}"]}
# Dedup check
if not skip_dedup:
dupes = find_near_duplicates(embedding, chunk["domain"], db_conn=db_conn)
if dupes:
return {
"status": "duplicate",
"similar_to": dupes[0]["id"],
"cosine_sim": dupes[0]["cosine_sim"],
}
# Insert
vec_str = f"[{','.join(str(v) for v in embedding)}]"
sql = """
INSERT INTO knowledge_chunks
(domain, topic, subtopic, content, summary,
source_citation, source_tier, embedding, token_count)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s::vector,%s)
ON CONFLICT DO NOTHING
RETURNING id
"""
with db_conn.cursor() as c:
c.execute(sql, (
chunk["domain"],
chunk["topic"],
chunk.get("subtopic"),
chunk["content"],
chunk["summary"],
chunk["source_citation"],
chunk["source_tier"],
vec_str,
estimate_tokens(chunk["content"]),
))
row = c.fetchone()
return {"status": "ok", "id": str(row[0])} if row else {"status": "conflict"}Step 5: Bulk Ingestion (Initial Load)
For loading large datasets, skip per-record dedup and use batched embeddings:
import json
from pathlib import Path
def load_all_chunks(data_dir: Path) -> list[dict]:
"""Load chunks from batch JSON files."""
all_chunks = []
for f in sorted(data_dir.glob("batch_*.json")):
data = json.loads(f.read_text())
chunks = data if isinstance(data, list) else data.get("chunks", [])
all_chunks.extend(chunks)
return all_chunks
def bulk_insert(chunks: list[dict], embeddings: list[list[float]], db_conn) -> dict:
"""Insert pre-embedded chunks in bulk."""
results = {"ok": 0, "errors": 0}
sql = """
INSERT INTO knowledge_chunks
(domain, topic, subtopic, content, summary,
source_citation, source_tier, embedding, token_count)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s::vector,%s)
ON CONFLICT DO NOTHING
"""
with db_conn.cursor() as c:
for chunk, emb in zip(chunks, embeddings):
try:
vec_str = f"[{','.join(str(v) for v in emb)}]"
c.execute(sql, (
chunk["domain"],
chunk["topic"],
chunk.get("subtopic"),
chunk["content"],
chunk["summary"],
chunk["source_citation"],
chunk["source_tier"],
vec_str,
estimate_tokens(chunk["content"]),
))
results["ok"] += 1
except Exception as e:
results["errors"] += 1
print(f" Insert error for {chunk.get('topic')}: {e}")
return results
def run_bulk_ingest(data_dir: Path, db_conn, batch_size: int = 100):
"""Full bulk ingestion pipeline."""
# Load
print("Loading chunks...")
all_chunks = load_all_chunks(data_dir)
print(f" Loaded {len(all_chunks)} chunks")
# Validate
print("Validating...")
good, bad = [], []
for chunk in all_chunks:
qa = validate_chunk(chunk)
(good if qa.valid else bad).append(chunk)
print(f" Valid: {len(good)}, Invalid: {len(bad)}")
# Embed in batches
print(f"Embedding {len(good)} chunks...")
all_embeddings = batch_embed(good, batch_size=batch_size)
# Insert
print(f"Inserting {len(good)} chunks...")
results = bulk_insert(good, all_embeddings, db_conn)
print(f" OK: {results['ok']}, Errors: {results['errors']}")
# Verify
with db_conn.cursor() as c:
c.execute("SELECT COUNT(*) FROM knowledge_chunks")
total = c.fetchone()[0]
c.execute("SELECT domain, COUNT(*) FROM knowledge_chunks GROUP BY domain")
by_domain = c.fetchall()
print(f"\nFinal DB count: {total}")
for domain, cnt in by_domain:
print(f" {domain}: {cnt}")Step 6: Batch Ingestion (Multiple Records, with Dedup)
For ongoing ingestion where dedup matters but you have multiple records:
def ingest_batch(chunks: list[dict], skip_dedup: bool = False, db_conn=None) -> dict:
"""Ingest a batch of chunks with per-record status tracking."""
results = {"ok": 0, "rejected": 0, "duplicate": 0, "errors": 0, "details": []}
for i, chunk in enumerate(chunks):
r = ingest_chunk(chunk, skip_dedup=skip_dedup, db_conn=db_conn)
results[r["status"]] = results.get(r["status"], 0) + 1
if r["status"] != "ok":
results["details"].append({"index": i, **r})
return resultspgvector Table Setup
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE knowledge_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain TEXT NOT NULL,
topic TEXT NOT NULL,
subtopic TEXT,
content TEXT NOT NULL,
summary TEXT NOT NULL,
source_citation TEXT NOT NULL,
source_tier TEXT NOT NULL,
source_year INTEGER,
embedding vector(1536), -- match your embedding model dimension
token_count INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Cosine similarity index for fast dedup and search
CREATE INDEX ON knowledge_chunks
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- Domain filter index
CREATE INDEX ON knowledge_chunks (domain);Common Pitfalls
- Embedding the wrong text — Use summary + content combined for richer embeddings, not just content alone
- No rate limiting — Add
time.sleep(0.2)between batches to avoid 429s from embedding APIs - Skipping validation — Invalid chunks waste embedding API calls; validate first
- Dedup threshold too low — 0.95 cosine similarity is a good starting point; lower catches more dupes but risks false positives
- ON CONFLICT DO NOTHING silently drops — Log conflicts so you know when inserts are being skipped
- Vector string formatting — pgvector expects
[0.1,0.2,...]format cast to::vector; don't pass Python lists directly - Token estimation — Track token counts per chunk so you can enforce context window limits during retrieval
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

