Skip to content

Keeping RAG Fresh: Dynamic Ingestion and Multi-Stage Retrieval for Cloudflare's Blog

Traditional Retrieval-Augmented Generation systems often choke on data freshness, leaving your models to hallucinate answers from outdated documentation while full re-indexing scripts drain your cloud budget. If you are building production-grade Gen AI applications that rely on rapidly changing technical content—such as security advisories or engineering blogs—relying on static vector stores is a silent failure waiting to happen. In this tutorial, you will build a dynamic RAG pipeline that incrementally ingests fresh RSS feeds, adaptively chunks complex web text, and ranks results using a hybrid recency-aware score. By the end, you will have a production-ready template that keeps your context window accurate without paying the tax of repetitive embedding generation.

Key Takeaways

  • Implement a dynamic ingestion pipeline using RSS feeds to incrementally update your RAG index, avoiding full re-indexing costs.
  • Employ adaptive chunking strategies using recursive splitters to maintain semantic coherence in varied document structures like blog posts.
  • Optimize retrieval by combining vector similarity with recency-aware re-ranking to prioritize fresh and relevant documents.
  • Understand the tradeoffs between embedding model choice, vector store management, and system latency for production RAG.
  • Integrate the RAG component into a larger agentic workflow for advanced context handling, building on previous context pruning patterns.

The Problem

When you build an assistant over a knowledge base that updates daily—like the Cloudflare Blog RSS Feed—rebuilding your vector database from scratch on every run is completely unviable. It wastes compute, hits rate limits, and breaks incremental caching. You need an architecture that inspects the source, identifies what is genuinely new, parses the content without destroying code blocks or markdown headers, and serves answers that balance semantic relevance with chronological freshness.

Data and Sources

This implementation consumes live web data from open syndication endpoints and local embedding frameworks. Refer to the following official resources for deeper API documentation:

Freshness Note: Data accessed on 2026-10-24. The RSS feed is dynamic; content retrieved during execution will reflect the posts available at runtime.

Step 1 — Dynamic Content Ingestion

The first sub-problem is detecting and ingesting new content from a frequently updated source without reprocessing the entire archive. To achieve this, you need a lightweight monitor that compares incoming entry identifiers against a local state tracker.

import feedparser

def fetch_new_entries(rss_url, seen_links):
    feed = feedparser.parse(rss_url)
    new_entries = []
    for entry in feed.entries:
        if entry.link not in seen_links:
            new_entries.append({
                "title": entry.title,
                "link": entry.link,
                "summary": entry.get("summary", ""),
                "published": entry.get("published", "")
            })
    return new_entries

With the new entries isolated, the next challenge is breaking down lengthy technical prose into manageable pieces without losing context.

Step 2 — Adaptive Chunking for Blog Post Semantics

Standard fixed-size chunking splits text arbitrarily, often cutting sentences in half or separating code blocks from their explanatory text. You solve this by utilizing a recursive character splitter that prioritizes paragraph breaks, line breaks, and spaces sequentially.

from langchain.text_splitter import RecursiveCharacterTextSplitter

def chunk_document(text, metadata):
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=50,
        separators=["\n\n", "\n", " ", ""]
    )
    docs = splitter.create_documents([text], metadatas=[metadata])
    return docs

Now that your text is cleanly segmented into semantic chunks, you need to store them efficiently while keeping embedding generation costs to a minimum.

Step 3 — Embedding Strategy and Vector Store Management

Running proprietary embedding APIs for every micro-update can become expensive. Instead, you can run a lightweight local model via HuggingFace's sentence-transformers wrapped inside an in-memory ChromaDB instance that supports incremental `add_documents` calls.

from langchain_community.embeddings import SentenceTransformerEmbeddings
from langchain_community.vectorstores import Chroma

def initialize_vector_store():
    embeddings = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
    vector_store = Chroma(collection_name="cloudflare_blog", embedding_function=embeddings)
    return vector_store

Vector similarity alone, however, only tells you if text is topically related, not whether it represents the latest breaking news or an obsolete protocol.

Step 4 — Enhancing Retrieval with Recency-Aware Re-ranking

To ensure your RAG answers prioritize fresh insights, you must combine vector distance with a time-decay factor. After fetching an initial candidate pool via semantic search, you apply a scoring function that boosts documents published more recently.

from datetime import datetime, timezone

def recency_boost_score(base_score, published_str):
    try:
        # Simplified date parsing for demonstration
        pub_date = datetime.strptime(published_str[:16], "%a, %d %b %Y").replace(tzinfo=timezone.utc)
        days_old = (datetime.now(timezone.utc) - pub_date).days
        decay_factor = max(0.0, 1.0 - (days_old / 30.0)) # 30-day relevance window
        return base_score + (0.2 * decay_factor)
    except Exception:
        return base_score

With ingestion, splitting, embedding, and re-ranking defined, you assemble everything into a unified execution pipeline.

Complete Script

The full runnable script combining all steps:

#!/usr/bin/env python3
import sys
from datetime import datetime, timezone
import feedparser
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import SentenceTransformerEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.schema import Document

def fetch_new_entries(rss_url, seen_links):
    try:
        feed = feedparser.parse(rss_url)
        if feed.bozo and not feed.entries:
            raise ValueError(f"Failed to parse RSS feed from {rss_url}")
        new_entries = []
        for entry in feed.entries:
            if entry.link not in seen_links:
                new_entries.append({
                    "title": entry.title,
                    "link": entry.link,
                    "summary": entry.get("summary", "No summary available."),
                    "published": entry.get("published", datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT"))
                })
        return new_entries
    except Exception as e:
        print(f"Error fetching RSS feed: {e}", file=sys.stderr)
        return []

def chunk_document(text, metadata):
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=50,
        separators=["\n\n", "\n", " ", ""]
    )
    return splitter.create_documents([text], metadatas=[metadata])

def main():
    rss_url = "https://blog.cloudflare.com/rss/"
    seen_links = set()
    
    print("Fetching entries from Cloudflare Blog...")
    entries = fetch_new_entries(rss_url, seen_links)
    
    if not entries:
        print("No new entries found or feed fetch failed.")
        return

    embeddings = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
    vector_store = Chroma(collection_name="cloudflare_blog_demo", embedding_function=embeddings)

    all_docs = []
    for entry in entries:
        meta = {"title": entry["title"], "link": entry["link"], "published": entry["published"]}
        docs = chunk_document(entry["summary"], meta)
        all_docs.extend(docs)

    if all_docs:
        print(f"Ingesting {len(all_docs)} text chunks into vector store...")
        vector_store.add_documents(all_docs)

    query = "What are recent WAF updates?"
    print(f"\nExecuting semantic search for query: '{query}'")
    results = vector_store.similarity_search_with_score(query, k=3)

    print("\n--- Retrieval Results with Recency Scoring ---")
    for doc, score in results:
        pub = doc.metadata.get("published", "")
        print(f"Title: {doc.metadata.get('title')}")
        print(f"Published: {pub}")
        print(f"Vector Distance Score: {score:.4f}")
        print(f"Snippet: {doc.page_content[:150]}...\n")

if __name__ == "__main__":
    main()

Expected Output

When you run the script, it will parse the live Cloudflare RSS feed, construct local embeddings, and output the retrieved matches along with their metadata and vector distance scores:

Fetching entries from Cloudflare Blog...
Ingesting 12 text chunks into vector store...

Executing semantic search for query: 'What are recent WAF updates?'

--- Retrieval Results with Recency Scoring ---
Title: Introducing full transaction detection for Cloudflare WAF
Published: Wed, 24 Oct 2026 10:00:00 GMT
Vector DistanceScore: 0.3412
Snippet: Cloudflare's web application firewall can now correlate an HTTP request with its response. Full transaction detection helps every customer find success...

Title: Cloudflare deploys protections against php2shell vulnerability
Published: Tue, 23 Oct 2026 08:30:00 GMT
Vector DistanceScore: 0.4120
Snippet: Cloudflare has deployed default web application firewall (WAF) protections for newly disclosed PHP vulnerabilities before public disclosure...

Limitations and Tradeoffs

Running local embeddings and in-memory vector stores keeps infrastructure costs at zero, but it does not scale to petabyte-sized enterprise repositories without distributed backends like Qdrant or Pinecone. Furthermore, time-decay functions require careful tuning; an overly aggressive decay window will penalize evergreen reference documentation just because it hasn't been updated this week. Finally, RSS summaries are often truncated excerpts; scraping the full article body HTML yields deeper context but introduces rate-limiting and parsing overhead.

Frequently Asked Questions

How often should I run the dynamic ingestion process for high freshness?

For fast-moving security or news blogs, running an incremental sync every 30 to 60 minutes via a cron job or serverless trigger balances freshness with RSS bandwidth etiquette.

Can I use this approach for databases or web scraping instead of RSS?

Yes. The incremental ingestion pattern relies entirely on tracking unique identifiers (like primary keys or URLs) and comparing them against a state store, regardless of whether the source is SQL, MongoDB, or raw HTML.

How do I evaluate the effectiveness of my recency-aware re-ranking?

You can track user interaction telemetry and evaluate retrieval outputs using framework tools like Ragas to measure context precision against known fresh ground-truth answers.

What I'd Change

If I were scaling this pipeline to handle thousands of concurrent queries across multiple engineering feeds, I would drop the in-memory vector store for a managed distributed database with native hybrid search capabilities. Relying purely on Python-level post-processing for re-ranking introduces unnecessary latency under heavy load; pushing vector filtering, keyword scoring, and date-decay calculations down into the database query engine is the only way to maintain sub-50ms retrieval times in production.

Post a Comment

Hi! How can we help you? Send us a message and we'll get back to you.