Retrieval-Augmented Generation (RAG) is how production AI systems answer with facts instead of guesses: retrieve relevant, current documents, then let the LLM generate grounded responses. But a RAG system is only as good as the data flowing into its vector database — and for most commercial use cases (pricing assistants, product Q&A bots, market intelligence copilots), that data lives on the open web and changes daily.
This tutorial from Actowiz Solutions walks through the complete pipeline: scraped web content → cleaning → structuring → chunking → embedding → vector DB ingestion → refresh. Code examples use Python with widely adopted tooling, and every stage reflects the patterns we run at production scale for enterprise clients.
A dataset is RAG-ready when retrieval works — meaning a user's question reliably pulls back the right passages. In practice that requires five properties:
Raw scraped HTML has none of these. The pipeline below adds all five.
[Scraper / Actowiz Feed]
│ raw JSON/HTML
▼
[1. Extraction & Cleaning] → boilerplate removal, text normalization
▼
[2. Structuring] → typed records + metadata
▼
[3. Deduplication] → hash + near-dup filtering
▼
[4. Chunking] → semantic, metadata-aware chunks
▼
[5. Embedding] → batch embed with retry logic
▼
[6. Vector DB Ingestion] → upsert with IDs (idempotent)
▼
[7. Refresh Loop] → scheduled re-crawl + delta upserts
The single biggest RAG shortcut: don't feed raw HTML to your pipeline. Start from structured extraction. A professionally scraped product record looks like this (illustrative sample):
{
"url": "https://example-retailer.com/product/earbuds-x",
"scraped_at": "2026-08-01T06:00:00Z",
"title": "Wireless Earbuds Model X",
"brand": "SampleBrand",
"price": 79.99,
"currency": "USD",
"description": "Noise-cancelling earbuds with 30-hour battery...",
"specs": {"battery_hours": 30, "bluetooth": "5.4"},
"reviews_summary": "4.4 stars from 12,847 reviews",
"category": "Electronics > Audio > Earbuds",
"language": "en"
}
Actowiz delivers feeds in exactly this shape (JSONL/Parquet), which lets you skip weeks of HTML-parsing fragility. If you're scraping in-house, use a robust extractor and keep the raw HTML separately for reprocessing.
Even structured text needs normalization before embedding:
import re, unicodedata
def clean_text(text: str) -> str:
text = unicodedata.normalize("NFKC", text)
text = re.sub(r"\s+", " ", text) # collapse whitespace
text = re.sub(r"(Cookie Policy|Accept All|Subscribe to newsletter).*", "", text, flags=re.I)
return text.strip()
Normalize currencies and units into metadata fields rather than leaving them in prose — "₹1,299" and "Rs. 1299" should both become {"price": 1299, "currency": "INR"} so retrieval can filter numerically.
Embedding costs money and duplicates poison retrieval. A two-tier approach works well:
import hashlib
from datasketch import MinHash, MinHashLSH
def exact_hash(text):
return hashlib.sha256(text.encode()).hexdigest()
lsh = MinHashLSH(threshold=0.85, num_perm=128)
def is_near_duplicate(doc_id: str, text: str) -> bool:
m = MinHash(num_perm=128)
for token in set(text.lower().split()):
m.update(token.encode())
if lsh.query(m):
return True
lsh.insert(doc_id, m)
return False
Exact hashes catch identical pages; MinHash-LSH catches template near-duplicates (the same product page across regional subdomains, for example). At web scale, expect to drop 30–40% of records here — that reduction improves answer quality.
Naive fixed-size chunking splits prices from product names and answers from questions. For commercial web data, chunk by record and by field, keeping metadata attached:
def chunk_product(record: dict) -> list[dict]:
base_meta = {
"url": record["url"],
"brand": record["brand"],
"category": record["category"],
"price": record["price"],
"currency": record["currency"],
"scraped_at": record["scraped_at"],
}
chunks = []
# Chunk 1: identity + price (always retrieved for price questions)
chunks.append({
"text": f"{record['title']} by {record['brand']}. "
f"Price: {record['price']} {record['currency']}. "
f"Category: {record['category']}.",
"meta": {**base_meta, "chunk_type": "identity"}
})
# Chunk 2+: description split at ~300 tokens with 40-token overlap
desc = clean_text(record["description"])
for i, piece in enumerate(split_tokens(desc, size=300, overlap=40)):
chunks.append({"text": piece,
"meta": {**base_meta, "chunk_type": "description", "part": i}})
return chunks
Rules of thumb we use in production: 200–400 tokens per chunk for product/listing data, 10–15% overlap, and always one compact "identity chunk" per record so entity-level questions retrieve cleanly.
def embed_batch(texts: list[str], client, model="your-embedding-model",
batch_size=96, max_retries=3):
vectors = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i+batch_size]
for attempt in range(max_retries):
try:
resp = client.embeddings.create(model=model, input=batch)
vectors.extend([d.embedding for d in resp.data])
break
except Exception:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
return vectors
Batching cuts cost and latency; exponential-backoff retries keep overnight ingestion jobs from dying on transient errors.
Use deterministic IDs so re-runs update rather than duplicate. The pattern is identical across Pinecone, Qdrant, Weaviate, Milvus, and pgvector:
def make_id(record_url: str, chunk_type: str, part: int = 0) -> str:
return hashlib.md5(f"{record_url}|{chunk_type}|{part}".encode()).hexdigest()
# Qdrant-style upsert (pattern is portable)
client.upsert(
collection_name="products",
points=[{
"id": make_id(c["meta"]["url"], c["meta"]["chunk_type"], c["meta"].get("part", 0)),
"vector": vec,
"payload": {**c["meta"], "text": c["text"]},
} for c, vec in zip(chunks, vectors)]
)
Store the text and metadata in the payload — retrieval-time filters (category == "Earbuds", scraped_at > yesterday) are what turn a demo into a product.
Commercial web data decays fast: prices change daily, listings expire, menus rotate. A production refresh loop has three parts:
def refresh(record: dict, stored_hash: str | None):
new_hash = exact_hash(record["description"] + str(record["price"]))
if new_hash == stored_hash:
return "skip"
upsert_chunks(chunk_product(record)) # re-embed only changed records
return "updated"
Everything upstream of Step 2 — resilient scraping, anti-bot handling, structured extraction, PII masking, compliance lineage — is what Actowiz Solutions industrializes. Clients receive RAG-optimized feeds: cleaned, deduplicated, metadata-rich JSONL/Parquet delivered to S3, GCS, Snowflake, or directly into your ingestion queue, on hourly to weekly cadences. Your team keeps Steps 4–7 (chunking, embedding, retrieval tuning) where your product logic lives, and skips the fragile part entirely.
For product, listing, and menu data, 200–400 tokens with 10–15% overlap works well, plus one compact "identity chunk" per record. Long-form articles tolerate larger chunks (400–600 tokens).
Match the data's volatility: hourly for prices and availability, daily for menus and listings, weekly for descriptive content. Use hash-based delta detection so only changed records are re-embedded.
The pipeline pattern in this tutorial is portable across Pinecone, Qdrant, Weaviate, Milvus, and pgvector. Choose based on your hosting, filtering, and scale requirements — the ingestion logic barely changes.
We deliver structured, deduplicated, metadata-rich feeds to your warehouse or object storage on your cadence, formatted for direct ingestion; many clients wire our JSONL drops straight into their embedding queue. Contact Actowiz Solutions for a pilot feed.
You can also reach us for all your mobile app scraping, data collection, web scraping , and instant data scraper service requirements!
Our web scraping expertise is relied on by 4,000+ global enterprises including Zomato, Tata Consumer, Subway, and Expedia — helping them turn web data into growth.
Watch how businesses like yours are using Actowiz data to drive growth.
From Zomato to Expedia — see why global leaders trust us with their data.
Backed by automation, data volume, and enterprise-grade scale — we help businesses from startups to Fortune 500s extract competitive insights across the USA, UK, UAE, and beyond.
We partner with agencies, system integrators, and technology platforms to deliver end-to-end solutions across the retail and digital shelf ecosystem.
Step-by-step tutorial by Actowiz Solutions: turn scraped web content into RAG-ready pipelines cleaning, chunking, embeddings & vector DB ingestion with Python code.
Grocery Price Comparison App Data helps track real-time prices, promotions, and retailer trends for smarter shopping and retail analytics.
Actowiz Solutions tracks Back-to-School 2026 pricing across Amazon, Walmart & Target — laptops, stationery & kidswear discount data, stock trends & category insights.
Whether you're a startup or a Fortune 500 — we have the right plan for your data needs.