Reference Pipeline Research Automation

PaperSearch arXiv Retrieval Pipeline

A reusable Python pipeline for turning arXiv search results into a local SQLite corpus of PDFs, extracted text, page chunks, and embeddings.

Problem Research agents need repeatable access to papers as structured local data, not one-off downloaded PDFs.
Outcome A SQLite-backed paper retrieval and processing pipeline that can feed semantic search, RAG, and downstream research agents.
Implementation evidence

The solution is backed by inspectable code

This solves the ingestion step for research automation: search arXiv, download matching PDFs, extract text, split it into reusable chunks, and keep the results in SQLite.

Code

CREATE TABLE IF NOT EXISTS query (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    query TEXT UNIQUE,
    datetime TEXT
);

CREATE TABLE IF NOT EXISTS paper_search (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    query TEXT,
    pdf_url TEXT,
    filename TEXT,
    pdf_data BLOB
);

CREATE TABLE IF NOT EXISTS document_page (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    paper_search_id INTEGER,
    page_number INTEGER,
    text TEXT,
    FOREIGN KEY (paper_search_id) REFERENCES paper_search(id)
);

CREATE TABLE IF NOT EXISTS document_page_split (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    document_page_id INTEGER,
    text TEXT,
    FOREIGN KEY (document_page_id) REFERENCES document_page(id)
);
import os
import sqlite3
import xml.etree.ElementTree as ET

import PyPDF2
import requests
from nltk.tokenize import sent_tokenize


class PaperSearch:
    def __init__(self, db_name="papers.db", data_dir="data", max_results=10, chunk_size=1200):
        self.base_url = "http://export.arxiv.org/api/query"
        self.max_results = max_results
        self.data_dir = data_dir
        self.chunk_size = chunk_size
        self.conn = sqlite3.connect(db_name)
        self.cursor = self.conn.cursor()

    def search(self, query):
        params = {"search_query": query, "start": 0, "max_results": self.max_results}
        response = requests.get(self.base_url, params=params, timeout=30)
        response.raise_for_status()
        os.makedirs(self.data_dir, exist_ok=True)

        downloaded = []
        for pdf_url in self.get_pdf_links(response.text):
            filename = self.download_pdf(pdf_url, query)
            if filename:
                downloaded.append(filename)
        return downloaded

    def get_pdf_links(self, response_text):
        root = ET.fromstring(response_text)
        links = []
        for entry in root.findall("{http://www.w3.org/2005/Atom}entry"):
            for link in entry.findall("{http://www.w3.org/2005/Atom}link"):
                if link.attrib.get("title") == "pdf":
                    links.append(link.attrib["href"])
        return links

    def download_pdf(self, pdf_url, query):
        self.cursor.execute("SELECT id FROM paper_search WHERE pdf_url = ?", (pdf_url,))
        if self.cursor.fetchone():
            return None

        filename = os.path.join(self.data_dir, pdf_url.rsplit("/", 1)[-1] + ".pdf")
        response = requests.get(pdf_url, timeout=60)
        response.raise_for_status()

        with open(filename, "wb") as f:
            f.write(response.content)

        self.cursor.execute(
            "INSERT INTO paper_search (query, pdf_url, filename, pdf_data) VALUES (?, ?, ?, ?)",
            (query, pdf_url, filename, response.content),
        )
        self.conn.commit()
        self.extract_text_from_pdf(filename)
        return filename

    def extract_text_from_pdf(self, pdf_file):
        self.cursor.execute("SELECT id FROM paper_search WHERE filename = ?", (pdf_file,))
        paper_search_id = self.cursor.fetchone()[0]

        with open(pdf_file, "rb") as file:
            reader = PyPDF2.PdfReader(file)
            for page_number, page in enumerate(reader.pages, start=1):
                text = page.extract_text() or ""
                self.cursor.execute(
                    "INSERT INTO document_page (paper_search_id, page_number, text) VALUES (?, ?, ?)",
                    (paper_search_id, page_number, text),
                )
                page_id = self.cursor.lastrowid
                self.split_text_on_sentences(page_id, text)
        self.conn.commit()

    def split_text_on_sentences(self, page_id, text):
        chunks, current = [], ""
        for sentence in sent_tokenize(text):
            if len(current) + len(sentence) <= self.chunk_size:
                current += " " + sentence
            else:
                chunks.append(current.strip())
                current = sentence
        if current:
            chunks.append(current.strip())

        self.cursor.executemany(
            "INSERT INTO document_page_split(document_page_id, text) VALUES (?, ?)",
            [(page_id, chunk) for chunk in chunks if chunk],
        )
        return chunks

Usage

search = PaperSearch(db_name="papers.db", data_dir="papers", max_results=20)
downloaded_files = search.search("retrieval augmented generation")

Requirements

Install requests, PyPDF2, and nltk. Run nltk.download("punkt") once if your environment does not already have the sentence tokenizer data.

Source

The article also points to the maintained implementation repository: ernanhughes/deepresearch.

Full explanation

For the full reasoning, database design, and development notes, read: Automating Paper Retrieval and Processing with PaperSearch.

The publishing loop Research → book → capstone → solution → real use → new evidence
Browse all solutions →