TL;DR: Use RAG as a preprocessing tool to improve your main RAG system by removing redundancy and noise from documents before indexing.
When building RAG (Retrieval-Augmented Generation) systems, we typically embed entire documents and store them in a vector database. But real-world documents have two major issues:
- Low signal-to-noise ratio: Documents contain filler, repeated content, and redundant information
- LLM token waste: We retrieve and process all that filler at inference time
This hurts both search quality and cost.
Use RAG to improve your RAG! Before indexing documents into your main vector database, we:
- Build a temporary vector index (FAISS) for each document's sentences
- Construct a similarity graph to identify central vs. redundant sentences
- Prune redundant content using greedy maximum coverage
- Index only the information-dense cleaned documents
We pay a small upfront cost at indexing time for better retrieval quality and lower inference costs.
Dataset: 1000 CNN/DailyMail articles
Compression: ~50% of sentences retained (e.g., 43 sentences → 16 kept)
Query: "Where were there magnitude 4 earthquakes?"
| Approach | Top Result | Second Result |
|---|---|---|
| Naive RAG | Magnitude 4 quake (relevant) | Drifted to irrelevant content |
| Micro RAG | Magnitude 4 quake (relevant) | Second magnitude 4 quake (relevant) |
The pruned index returns more focused, information-dense results.
pip install sentence-transformers faiss-cpu pandas datasetsOr run the first cell in the notebook to install in Google Colab:
!pip install -q sentence-transformers faiss-cpu pandasimport numpy as np
import faiss
import pandas as pd
from sentence_transformers import SentenceTransformer
import reWe use:
- sentence-transformers: For embedding text
- FAISS: For fast similarity search (our "micro RAG")
- pandas: For result comparison
- datasets: To load real documents (CNN/DailyMail)
def split_into_sentences(text):
"""Simple sentence splitter using regex."""
raw_sentences = re.split(r'(?<=[.!?])\s+', text.strip())
sentences = [s.strip() for s in raw_sentences if len(s.strip()) > 0]
return sentences
# Load embedding model
model_name = "sentence-transformers/all-MiniLM-L6-v2"
embedder = SentenceTransformer(model_name)
def embed_texts(texts, batch_size=32):
"""Embed texts and return numpy array."""
return embedder.encode(texts, batch_size=batch_size,
show_progress_bar=False,
convert_to_numpy=True)Key Point: We split documents into sentences because we want to identify and remove redundant sentences, not entire paragraphs.
from datasets import load_dataset
# Load CNN/DailyMail news articles
dataset = load_dataset("cnn_dailymail", "3.0.0", split="train[:1000]")
raw_docs = [x["article"] for x in dataset]
# Filter and clean documents
docs = []
for d in raw_docs:
text = d.strip()
if len(text) < 1000: # Skip very short docs
continue
if len(text) > 8000: # Truncate very long docs
text = text[:8000]
docs.append(text)We load 1000 real news articles to demonstrate the technique on realistic data.
# Embed full documents as-is
naive_embeddings = embed_texts(docs)
# Normalize embeddings for cosine similarity
naive_norms = np.linalg.norm(naive_embeddings, axis=1, keepdims=True) + 1e-10
naive_embeddings_norm = naive_embeddings / naive_norms
# Create FAISS index
d = naive_embeddings_norm.shape[1]
naive_index = faiss.IndexFlatIP(d) # Inner product = cosine sim with normalized vectors
naive_index.add(naive_embeddings_norm.astype("float32"))
# Store metadata
naive_meta = [
{"doc_id": i, "text": docs[i]}
for i in range(len(docs))
]This is the standard RAG approach: embed full documents and index them.
This is where we use RAG to improve RAG. For each document:
K_NEIGHBORS = 5 # top-K neighbors per sentence (local graph)
COVERAGE_BUDGET_PCT = 0.5 # Keep ~50% of sentences
cleaned_docs = []
for doc_id, doc in enumerate(docs):
# Step 1: Split into sentences
sentences = split_into_sentences(doc)
n = len(sentences)
# Step 2: Embed all sentences
sent_embs = embed_texts(sentences)
norms = np.linalg.norm(sent_embs, axis=1, keepdims=True) + 1e-10
sent_embs_norm = sent_embs / normsKey Insight: Instead of comparing every sentence to every other sentence (O(N²)), we use FAISS to find top-K neighbors (O(N)):
# Step 3: Build transient FAISS index (THIS IS THE MICRO RAG!)
dim = sent_embs_norm.shape[1]
temp_index = faiss.IndexFlatIP(dim)
temp_index.add(sent_embs_norm.astype("float32"))
# Step 4: For each sentence, get top-K similar neighbors
k = min(K_NEIGHBORS, n)
sims, idxs = temp_index.search(sent_embs_norm.astype("float32"), k)
# Build neighbor lists with similarity weights
neighbors = []
for i in range(n):
nbrs = []
for sim_val, j in zip(sims[i], idxs[i]):
j = int(j)
nbrs.append((j, float(sim_val)))
neighbors.append(nbrs)Now we have a similarity graph where each sentence knows its top-K most similar neighbors.
We select sentences that best "cover" the document's semantic space:
# Step 5: Greedy maximum coverage
coverage_budget = max(1, int(COVERAGE_BUDGET_PCT * n))
covered = np.zeros(n, dtype=bool) # Which sentences are "covered"
selected_mask = np.zeros(n, dtype=bool) # Which sentences we've selected
selected_indices = []
while len(selected_indices) < coverage_budget:
best_i = None
best_gain = -1.0
# Find the sentence that covers the most uncovered content
for i in range(n):
if selected_mask[i]:
continue
gain = 0.0
for j, w in neighbors[i]:
if not covered[j]:
gain += w # Weight by similarity
if gain > best_gain + 1e-8:
best_gain = gain
best_i = i
if best_i is None or best_gain <= 0:
break
# Select this sentence
selected_indices.append(best_i)
selected_mask[best_i] = True
# Mark neighbors as covered
for j, w in neighbors[best_i]:
covered[j] = TrueHow it works:
- Each sentence "covers" itself and its similar neighbors
- We greedily select sentences that cover the most uncovered content
- Sentences similar to already-selected ones contribute less
- Redundant sentences naturally get pruned
# Step 6: Rebuild document from selected sentences (in original order)
keep_indices = sorted(selected_indices)
cleaned_sentences = [sentences[i] for i in keep_indices]
cleaned_doc = " ".join(cleaned_sentences)
cleaned_docs.append(cleaned_doc)Finally, index the cleaned documents:
# Embed cleaned documents
pruned_embeddings = embed_texts(cleaned_docs)
pruned_norms = np.linalg.norm(pruned_embeddings, axis=1, keepdims=True) + 1e-10
pruned_embeddings_norm = pruned_embeddings / pruned_norms
# Create FAISS index
pruned_index = faiss.IndexFlatIP(pruned_embeddings_norm.shape[1])
pruned_index.add(pruned_embeddings_norm.astype("float32"))
pruned_meta = [
{"doc_id": i, "text": cleaned_docs[i]}
for i in range(len(cleaned_docs))
]def search_index(query, index, embeddings_norm, meta, top_k=5):
"""Search a FAISS index and return top-k results."""
q_emb = embed_texts([query])[0]
q_emb = q_emb / (np.linalg.norm(q_emb) + 1e-10)
sims, idxs = index.search(q_emb.reshape(1, -1).astype("float32"), top_k)
results = []
for rank, (i, s) in enumerate(zip(idxs[0], sims[0]), start=1):
row = meta[int(i)].copy()
row["rank"] = rank
row["score"] = float(s)
results.append(row)
return results
def compare_naive_vs_pruned(query, top_k=5):
"""Compare naive and pruned retrieval side-by-side."""
naive_results = search_index(query, naive_index, naive_embeddings_norm,
naive_meta, top_k=top_k)
pruned_results = search_index(query, pruned_index, pruned_embeddings_norm,
pruned_meta, top_k=top_k)
# Format results in a comparison table
rows = []
for r in range(top_k):
naive_row = naive_results[r] if r < len(naive_results) else {}
pruned_row = pruned_results[r] if r < len(pruned_results) else {}
rows.append({
"Rank": r + 1,
"Naive_score": naive_row.get("score", None),
"Naive_preview": naive_row.get("text", "")[:260] + "...",
"Pruned_score": pruned_row.get("score", None),
"Pruned_preview": pruned_row.get("text", "")[:260] + "...",
})
return pd.DataFrame(rows)query = "where were there magnitude 4 earthquakes"
df_compare = compare_naive_vs_pruned(query, top_k=5)
display(df_compare)You'll see that the pruned index returns more focused, relevant results!
- RAG is a tool, not just a product: We use vector similarity search to solve the sentence deduplication problem
- O(N) instead of O(N²): FAISS gives us approximate nearest neighbors efficiently
- Pay upfront, save later: Small cost at indexing time → better retrieval + lower inference costs
- Graph-based coverage: The similarity graph captures which sentences are redundant
- Preserve order: We keep sentences in their original order so the document still reads naturally
- Large documents with redundancy (news, reports, legal docs)
- Cost-sensitive applications (fewer tokens to LLM)
- Quality-focused retrieval (better signal-to-noise)
Tune these parameters for your use case:
K_NEIGHBORS: How many similar sentences to consider (default: 5)COVERAGE_BUDGET_PCT: What % of sentences to keep (default: 0.5 / 50%)- Coverage algorithm: Try other graph algorithms (PageRank, clustering, etc.)
- Open the
microrag.ipynbnotebook in Colab - Run cells sequentially
- Experiment with different queries and datasets!
# Clone the repo
git clone https://github.com/YOUR_USERNAME/microrag-demo.git
cd microrag-demo
# Install dependencies
pip install sentence-transformers faiss-cpu pandas datasets
# Open notebook
jupyter notebook microrag.ipynbRAG is more than a corporate product; it's a tool for success.
Don't treat RAG as a monolithic architecture. It's semantic similarity search—use it wherever you need to compare embeddings. Here we used it to preprocess documents, but you could use "micro RAG" for:
- Deduplicating knowledge bases
- Finding similar code snippets within a function
- Clustering similar user queries
- Any scenario where you need to understand semantic relationships
If you use this technique, please link back to this repo or cite it in your work!
@misc{microrag2025,
title={Micro RAG: Using RAG to Improve RAG},
author={Your Name},
year={2025},
howpublished={\url{https://github.com/YOUR_USERNAME/microrag-demo}},
}MIT License - feel free to use this in your projects!
Found a better coverage algorithm? Have ideas for optimizations? PRs welcome!
Remember: Don't put baby in a corner. RAG is a versatile tool—use it creatively! 🚀