In the era of fast-paced news, staying updated while understanding the context behind every article is challenging. Retrieval-Augmented Generation (RAG) systems solve this problem by combining vector search and language models to provide accurate, contextual answers to user queries.
In this mini hack, we’ll build a News Search and Question-Answering system using LangChain, FAISS, and OpenAI’s GPT-4 (gpt-40-mini
). The system allows users to:
- Search for news articles by semantic description
- Ask questions about specific articles
- Get concise, accurate answers in natural language
We will implement three core classes: ArticleLoader
, ArticleRetriever
, and GenerateResponse
.
1. ArticleLoader
The ArticleLoader
class is responsible for reading the news dataset and converting each article into LangChain Document objects. It also splits articles into chunks for more efficient retrieval.
import os
from typing import List
from langchain_core.documents import Document
from langchain.text_splitter import RecursiveCharacterTextSplitter
class ArticleLoader:
def __init__(self, path: str):
"""Initialise file path."""
self.path = path
def document_load(self) -> list[Document]:
"""
Load articles from a text file.
Raises FileNotFoundError if the file is not found.
"""
if not os.path.exists(self.path):
raise FileNotFoundError(f"File not found: {self.path}")
with open(self.path, "r", encoding="utf-8") as file:
text = file.read().strip()
# Split articles by blank lines
raw_articles = [a.strip() for a in text.split("\n\n") if a.strip()]
documents: list[Document] = [
Document(page_content=article, metadata={}) for article in raw_articles
]
return documents
def create_chunks(self, documents: list[Document]) -> list[Document]:
"""
Split the documents into chunks of size 500 with overlap of 50.
"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
length_function=len
)
return text_splitter.split_documents(documents)
2. ArticleRetriever
The ArticleRetriever
class handles embedding creation, FAISS vector storage, and building a retriever to fetch the most relevant documents based on a query.
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.chat_models import ChatOpenAI
from langchain_core.vectorstores.base import VectorStoreRetriever
class ArticleRetriever:
def __init__(self):
"""Initialize vector store with OpenAI embeddings."""
self.embeddings = OpenAIEmbeddings()
def create_retriever(self, text_chunks) -> VectorStoreRetriever:
"""
Create FAISS vector store from documents.
Returns a retriever that returns top 4 relevant documents.
"""
vectorstore = FAISS.from_documents(text_chunks, self.embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
return retriever
def load_model(self) -> ChatOpenAI:
"""
Initialize and return LLM model with gpt-40-mini.
"""
return ChatOpenAI(model="gpt-40-mini")
3. GenerateResponse
The GenerateResponse
class connects the retriever and language model to generate answers for user queries. It also ensures that the output is returned as a clean string.
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough
class GenerateResponse:
def __init__(self, retriever):
"""
Initialize the generator with retriever and output parser.
"""
self.retriever = retriever
self.output_parser = StrOutputParser()
def create_ragchain(self, model):
"""
Create a Retrieval-Augmented Generation (RAG) chain.
"""
prompt = ChatPromptTemplate.from_template(
"Answer the question based on the context:\n\n"
"Context:\n{context}\n\n"
"Question:\n{query}\n\n"
"Answer:"
)
rag_chain = (
{"context": self.retriever, "query": RunnablePassthrough()}
| prompt
| model
| self.output_parser
)
return rag_chain
def generate_relevant_text(self, query: str, rag_chain) -> str:
"""
Query the RAG chain to get response as a string.
Raises ValueError if query is empty or too short.
"""
if not query or len(query.strip()) < 10:
raise ValueError("Query too short.")
response = rag_chain.invoke(query)
if hasattr(response, "content"):
return response.content
elif isinstance(response, str):
return response
else:
return str(response)
Conclusion
With these three classes, you now have a fully functional RAG-based news summarizer.
The workflow is:
- Load articles → ArticleLoader.document_load()
- Chunk articles → ArticleLoader.create_chunks()
- Create retriever → ArticleRetriever.create_retriever()
- Load LLM → ArticleRetriever.load_model()
- Create RAG chain → GenerateResponse.create_ragchain()
- Ask a question → GenerateResponse.generate_relevant_text(query, rag_chain)
This setup allows you to query any news dataset, get contextually accurate answers, and even summarize insights quickly — making it a perfect mini hack for data-driven news applications.