How it works
Complete reference for Telegram Search Engine: architecture, configuration, pipeline commands, API endpoints, and how search, graphs, and scoring work.
# Overview
A self-hosted engine that discovers, analyzes, ranks, and maps public Telegram channels. It reads public channels with a residential IP, classifies them with a local LLM, scores by quality + activity + influence + freshness, and builds an interactive graph of how they reference each other.
Open source, fully self-hosted. One docker compose up runs the whole stack. Use as a data layer for structured Telegram data, or run the web UI as a discovery product.
This demo is read-only over a frozen snapshot of tech-community channels.
# Architecture
Two deliberately separated layers:
- Pipeline (crawl → analyze → graph): runs on your machine with residential IP + GPU. Writes to Postgres.
- Serving layer (web + API + search): runs anywhere via Docker, reads only.
YOUR MACHINE (residential IP + GPU)
ingestion (Telethon) ──┐
analysis (Ollama) ──┼──► Postgres ──► API (FastAPI) ──► web (Next.js)
graph (networkx) ──┘ └─────► MeilisearchPipeline reads public channels with Telethon, stores in Postgres, analyzes with Ollama, computes graph metrics with networkx. Serving layer reads Postgres / Meilisearch, exposes HTTP API, serves web UI.
# Configuration
Set environment variables in .env (dev) or .env.prod (production).
Required
--print-session)postgresql://user:pass@localhost/tg_db)Telegram Crawl
LLM & Search
API Server
# Pipeline Commands
All commands run from repo root with venv active. From your machine with residential IP + GPU.
0. Capture Telegram Session (one-time)
python -m app.ingestion.crawl --print-sessionInteractive login. Copy printed session string into TG_SESSION_STRING in .env.
1a. Crawl by Keywords
python -m app.ingestion.crawl --keywords phones addis crypto jobsSearch each keyword, deduplicate by tg_id, sample ~40 messages, store in Postgres, enqueue discovered refs into frontier (depth 1).
1b. DB-driven Keyword Expansion
python -m app.ingestion.crawl --from-db --max-queries 20 --min-age-hours 24Generate bases × modifiers, pick due queries (crawled >24h ago), run them, record in keyword_runs. Enables iterative discovery without re-crawling.
1c. Seed a Known Channel
python -m app.ingestion.add_channel solodevchronicles
python -m app.ingestion.add_channel https://t.me/SoloDevChronicles
python -m app.ingestion.add_channel @channel1 https://t.me/channel2Add channels to frontier at depth 0. Picked up by next --link-graph crawl. Accepts usernames, @handles, t.me URLs.
2. Link-graph Discovery
python -m app.ingestion.crawl --link-graph --max-depth 2 --limit 30
python -m app.ingestion.crawl --link-graph --limit 5 --messages 1000Drain frontier queue: resolve candidates, sample messages, extract edges, enqueue refs up to max-depth.
3. Analyze with LLM
python -m app.analysis.run --limit 200Pull un-analyzed channels, classify with Ollama, compute quality/activity/freshness scores, upsert analysis, mirror into Meilisearch.
4. Compute Graph Metrics
python -m app.graph.metricsBuild networkx directed graph from edges, compute PageRank/betweenness/Louvain clusters, write to channel_graph, rescore all channels with influence, re-sync Meili.
5. Backfill Edges
python -m app.graph.backfill_edgesRebuild edges from stored messages (no Telegram API calls). Extracts t.me links and @mentions from message text. Useful after bulk imports.
6. Reindex Search
python -m app.search.reindexBulk-load all analyzed channels from Postgres into Meilisearch. Use once after setup or to rebuild index.
7. Run API Server
uvicorn app.api.main:app --reload --port 8000Start read-only FastAPI server. All endpoints are GET. CORS restricted to CORS_ORIGINS.
# API Reference
Base: http://localhost:8000. All endpoints are read-only GET.
GET /health
Health check.
curl http://localhost:8000/health
# {"status": "ok"}GET /search
Search channels by query. Typo-tolerant ranking via Meilisearch (or Postgres FTS fallback).
curl "http://localhost:8000/search?q=crypto&limit=10"
curl "http://localhost:8000/search?q=phones+addis&limit=50"GET /channel/{channel_id}
Full channel detail: metadata, sample messages (20), analytics, all scores.
curl http://localhost:8000/channel/123GET /categories
List all categories with channel counts.
curl http://localhost:8000/categoriesGET /stats
Pipeline statistics: total channels, analyzed, frontier status.
curl http://localhost:8000/statsGET /graph
Channel reference graph: nodes + edges. Paginated by score, optionally filtered by cluster.
curl "http://localhost:8000/graph?limit=500"
curl "http://localhost:8000/graph?limit=250&cluster_id=5"GET /graph/hubs
Most influential channels (highest PageRank).
curl "http://localhost:8000/graph/hubs?limit=50"GET /graph/bridges
Channels bridging communities (highest betweenness centrality).
curl "http://localhost:8000/graph/bridges?limit=20"GET /graph/clusters
Louvain communities with aggregate stats and top channels.
curl http://localhost:8000/graph/clusters# How Search Works
Search ranks via Meilisearch (typo tolerance, word/proximity), with automatic fallback to Postgres FTS if Meili is down or unconfigured.
Meilisearch Mode
Postgres FTS Fallback
# How Graph Works
Channel-to-channel references become weighted directed edges. Graph metrics compute influence, bridges, and communities.
Edge Types
Computation
# How Scoring Works
final_score = quality·40% + activity·30% + influence·20% + freshness·10%Computed by analyzer on first analysis. Graph metrics recompute influence and refresh final_score for all channels. Scores synced into Meili for search ranking.
# Safety / ToS
Crawler is intentionally read-only and throttled. Never joins, honors rate limits, runs on dedicated aged account on residential IP.
Bans come from behavior (mass-join, ignoring limits), not from reading public channels carefully. Keep crawl volume modest. Store session string for fast swaps.
# Deployment
See DEPLOY.md in repo for single-VPS Docker deploy (Caddy + HTTPS), MIGRATE_DB.md for moving data into dockerized Postgres, and server-infra/README.md for multi-project reverse proxy setup.
git clone github.com/your/telegram-search-engine
cp .env.prod.example .env.prod
# fill in: TG_API_ID, TG_API_HASH, TG_SESSION_STRING, DATABASE_URL, etc.
docker compose --env-file .env.prod up -d --build