Skip to content

Repository files navigation

FinanceRAG

Ask questions about recent market news and get answers grounded in retrieved sources.

Live: https://finance-rag-one.vercel.app

Yahoo Finance headlines are ingested, embedded into pgvector, and searched by semantic similarity. Every answer is composed only from the passages retrieved for that question, and those passages are printed underneath it so the answer can be checked.

Stack

FastAPI · Neon Postgres · pgvector · Gemini (gemini-embedding-2, gemini-flash-lite-latest) · yfinance

Runs entirely on free tiers.

What's interesting here

  • Fresh beats similar. Ranking is not similarity alone. Every passage carries a distance penalty that grows with age — none when new, approaching RECENCY_WEIGHT when ancient — so a stale story that matches well loses to a current one that matches nearly as well. Backdate the top hit by 60 days and it falls out of the results while its distance never changes. For a news corpus this is the difference between answering about today's market and answering about last quarter's.
  • The model declines rather than reaching. Ask it about Nvidia when the corpus has no Nvidia coverage and it says so, then describes the AMD reporting it did find. The answer is built from the retrieved passages alone, never from what the model happens to remember. Passages reach it dated, alongside today's date, so it can lead with the most recent.
  • Questions and passages are embedded differently. Passages go in as RETRIEVAL_DOCUMENT, questions as RETRIEVAL_QUERY. They are not interchangeable — embedding both as the same thing measurably degrades retrieval.
  • Duplicate passages are impossible, not merely discouraged. A unique index on md5(chunk) plus ON CONFLICT DO NOTHING means re-running ingest adds nothing. Without it a k=3 search happily returned three copies of one story as if they were three sources.
  • 768 dimensions, not 3072. gemini-embedding-2 is matryoshka, so the shorter vector is a truncation rather than a weaker model, and it arrives pre-normalised — which is why an L2 (<->) ordering ranks identically to cosine here.

Run locally

git clone https://github.com/kaniikaaaa/FinanceRAG.git
cd FinanceRAG

python -m venv venv && source venv/bin/activate    # Windows: venv\Scripts\activate
pip install -r requirements.txt

cp .env.example .env    # then fill in DATABASE_URL and GEMINI_API_KEY

Needs a Postgres with pgvector available — Neon's free tier has it built in. A free Gemini key comes from AI Studio.

python -m app.migrate               # extension, table, embedding column, unique index
python -m app.ingest AAPL TSLA      # pull news (defaults to AAPL TSLA MSFT NVDA)
python -m app.embed                 # embed anything not yet embedded
uvicorn app.main:app --reload

Run migrate first: it owns the schema. ingest and embed are both safe to re-run — ingest skips passages it already has, embed skips rows it has already embedded.

Keeping the corpus fresh

.github/workflows/ingest.yml runs migrate → ingest → embed against the same database every six hours, and on demand from the Actions tab. It needs DATABASE_URL and GEMINI_API_KEY as repository secrets.

This is the write path, and it deliberately does not live on Vercel: embedding is a batch job measured in minutes, which is the one thing a serverless function's timeout cannot accommodate. Vercel serves the read path, where a query is three network calls and no state of its own.

API

GET  /               the web UI (?q=... files a query directly)
GET  /api/health     status + the models actually in use
GET  /api/headlines  recent titles, for the ticker
POST /ask            { "question": "what's driving chip stocks?" }

/api/health deliberately touches nothing but memory — it is the deploy health check, and making it depend on Postgres would report the app as down whenever Neon is merely asleep.

/ask returns:

{
  "question": "...",
  "answer": "...",
  "sources": [
    {
      "title": "...",
      "content": "...",
      "published_at": "2026-07-16T22:17:00+00:00",
      "url": "https://..."
    }
  ]
}

published_at falls back to fetch time for passages stored before publish dates were captured — it is the same date the ranking scores on.

Failures come back as {"detail": "..."} with a status that says where it broke: 503 if the database is unreachable, 502 if retrieval or the model failed, 404 if nothing in the corpus matched.

Module layout

app/
├── main.py     FastAPI app — serves the UI and /ask
├── db.py       connection (prefers DATABASE_URL, falls back to DB_* parts)
├── migrate.py  owns the schema; run before ingest
├── ingest.py   yfinance news fetcher
├── embed.py    backfills embeddings
├── search.py   vector search + answer composition
├── gemini.py   Gemini client, model names, dimensions
└── vectors.py  renders an embedding as a pgvector literal
web/
└── index.html  the single-page UI

Deploying

Both targets need the same two environment variables, DATABASE_URL and GEMINI_API_KEY, and neither can read .env — that file never leaves your machine.

Render (render.yaml). New → Blueprint → pick this repo. Render reads the file and asks for the two secrets, since they are marked sync: false and so are never committed. Runs uvicorn as an ordinary long-lived process. On the free plan the service sleeps after 15 minutes idle and the next request pays roughly a minute to wake it.

Vercel. Autodetects app/main.py, so it needs no entrypoint config — adding a pyproject.toml actually breaks the build, because its presence switches Vercel from requirements.txt to uv, which then demands a [project] table. Set both variables in Project Settings → Environment Variables and redeploy; a variable only reaches a new build, never an existing deployment.

Nothing in the code knows which one it is running on. The app holds no state: it embeds a question, runs one query against Neon, and asks Gemini to write the answer. The vectors live in Postgres and the models live behind an API, so the process itself is disposable — which is why it runs equally well as a container or as a function.

License

MIT

About

RAG over Yahoo Finance news. Recency-weighted retrieval, cited answers, pgvector + GPT-4o-mini.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages