PyCrawler is a distributed web crawler and full-text search engine built with Python 3.13, FastAPI, Redis, and PostgreSQL. The project is designed to demonstrate advanced concepts in distributed message queues, horizontal scaling, information retrieval (PageRank + TF-IDF), semantic vector search, and conditional caching headers.
It features an interactive Single Page Dashboard (SPA), allowing users to control the crawler, view real-time statistics, search the database, and inspect crawled page content.
- Distributed Async Crawling: Asynchronous worker loops coordinate via Redis.
- Redis Task Queue: Horizontal worker scaling via Redis
LISTand visited history tracking via RedisSET. - robots.txt Compliance: Politeness delay management and checker caching.
- Incremental Crawling: HTTP conditional headers (
If-None-Match/If-Modified-Since) skip unchanged pages with304 Not Modified. - Duplicate Detection: Document body hashing using SHA-256 to prevent duplicate indices.
- PostgreSQL Storage: Reliable async database persistence using SQLAlchemy +
asyncpg. - PageRank Link Analysis: Global link authority score calculation using mathematical dangling node correction.
- TF-IDF + Semantic Search: Blended candidate scores matching exact terms and vector embeddings (
all-MiniLM-L6-v2). - Interactive Dashboard: Real-time progress monitoring, logs, and query recommendation box.
- Dockerized Deployment: Clean orchestration using multi-stage builds.
- Automated Tests: Unit testing suite running automatically via GitHub Actions CI.
- Backend: FastAPI, Uvicorn, Python 3.13
- Distributed Queue: Redis 7 (persistent)
- Database: PostgreSQL 16
- HTML Parsing: BeautifulSoup4 +
lxml - ORM & Connection: SQLAlchemy 2.0 (Async) +
asyncpg - Embeddings Model: Sentence-Transformers
all-MiniLM-L6-v2 - Testing: Python
unittest(automated pipeline via GitHub Actions) - Frontend: Vanilla HTML5, CSS (Dark-mode theme), Javascript
flowchart TD
subgraph Client [User Interface]
UI[Interactive Dashboard SPA]
end
subgraph Service [FastAPI App Layer]
API[API Endpoints]
CM[Crawler Manager]
end
subgraph HotQueue [Redis Queue Layer]
RQ[(Task Queue LIST)]
VS[(Visited Set SET)]
WB[(Worker Heartbeats)]
end
subgraph Workers [Async Worker Pool]
W1[Worker 1]
W2[Worker 2]
Parser[BS4 HTML Parser]
Embed[Sentence-Transformers]
end
subgraph ColdStorage [PostgreSQL Database]
DB_Pages[(Pages / Links)]
DB_Index[(Inverted Index)]
DB_Vector[(Embeddings Table)]
end
UI <-->|REST / Search / Control| API
API <--> CM
CM <-->|Starts / Stops| Workers
W1 & W2 <-->|BRPOP Task / SADD Visited| HotQueue
W1 & W2 -->|Save HTML & Links| DB_Pages
W1 & W2 -->|Tokenize & Index| DB_Index
W1 & W2 -->|Compute Vector| Embed
Embed -->|Save Vector| DB_Vector
The flowchart below demonstrates the lifecycle of a URL task as it is processed by an active background worker:
flowchart TD
Start([URL in Queue]) --> Dequeue[Worker BRPOP URL from Redis LIST]
Dequeue --> CheckSet{SADD Visited SET\nSuccessfully?}
CheckSet -->|No: Already Visited| Discard([Discard URL])
CheckSet -->|Yes: New URL| CheckRobots{Respects robots.txt\n& Crawl Delay?}
CheckRobots -->|No: Disallowed| Discard
CheckRobots -->|Yes: Allowed| FetchHTML[Fetch HTML Page via HTTP]
FetchHTML --> Check304{Status 304\nNot Modified?}
Check304 -->|Yes| UpdateTimestamp[Update DB last_checked]
UpdateTimestamp --> Discard
Check304 -->|No: 200 OK| ParseHTML[Extract Content, Meta, & Links via lxml]
ParseHTML --> CheckDup{Content Hash\nExists in DB?}
CheckDup -->|Yes| Discard
CheckDup -->|No| StorePage[Save Page to PostgreSQL]
StorePage --> Parallel[Index Page Content]
subgraph Indexing [Page Indexing Pipeline]
Parallel --> InvertedIndex[Tokenize & Bulk Insert Words\nON CONFLICT DO NOTHING]
Parallel --> Embeddings[Generate Semantic Embedding\nasyncio.to_thread]
end
InvertedIndex --> QueueLinks[Push Extracted Links to Redis LIST\nif Depth < MAX_DEPTH]
Embeddings --> QueueLinks
QueueLinks --> End([Done])
pycrawler/
βββ .github/
β βββ workflows/ # CI workflow running automated tests on commit
βββ app/
β βββ api/ # REST API endpoints (search, seeds, crawl controls, stats)
β βββ config/ # Pydantic settings loading configuration
β βββ crawler/ # Async workers & robots.txt checker
β βββ database/ # Async database sessions & pool initialization
β βββ indexing/ # Word extraction, TF calculator, & embedding upserts
β βββ models/ # SQLAlchemy relational database models
β βββ parser/ # HTML parsed content, links, & SHA-256 hashing
β βββ queue/ # Redis queue controller & DB fallback managers
β βββ ranking/ # Blended Ranker (TF-IDF + PageRank + Cosine Similarity)
β βββ search/ # Alphanumeric tokenizer & custom stemmer
β βββ dashboard/ # Front-end dashboard user interface
βββ docs/
β βββ screenshots/ # UI screenshots
βββ scripts/
β βββ generate_mock_corpus.py # Local mock corpus generator
βββ tests/ # Automated test suite (tokenizer, parser, ranker, pagerank)
βββ Dockerfile # Container setup utilizing the uv package manager
βββ docker-compose.yml# Coordinated service stack configuration
βββ requirements.txt # Python requirements list
βββ README.md # Documentation
Make sure you have Docker and Docker Compose installed on your machine.
Start the compose stack from the project root:
docker-compose down -v
docker-compose up --buildThis command automatically:
- Boots persistent PostgreSQL and Redis containers.
- Creates the schema and sets up database tables.
- Starts the FastAPI web server on
http://localhost:8080/.
Open your web browser and navigate to:
http://localhost:8080/
- Add a seed URL (e.g.,
http://host.docker.internal:8090/page_0.htmlfor local benchmarks) and click Add. - Click Start Crawler to initiate concurrent worker loops.
- Wait for pages to crawl, then click Compute PageRank to calculate link authority scores.
- Input queries in the search box. Suggestions will load dynamically. Press Enter to view ranked results.
To run the unit tests inside a local virtual environment:
- Create a virtual environment and install packages:
python -m pip install uv
- Set up virtual environment and install dependencies:
python -m uv venv .venv .venv\Scripts\activate # On Windows # source .venv/bin/activate on Linux/macOS python -m uv pip install -r requirements.txt
- Run the test suite:
.venv\Scripts\python -m unittest discover -s tests -v
To enable horizontally scalable crawling without duplication, PyCrawler coordinates tasks using a Redis-backed queue system.
- Horizontal Coordination: Workers pop URLs using
BRPOP(blocking list pop), guaranteeing that exactly one worker processes each task. - O(1) Deduplication: Before a URL is processed, workers perform an
SADDoperation on a Redis visited set. Since this runs in memory at sub-millisecond speeds, workers skip duplicate links instantly without performing slow SQL queries. - Resilience: Redis data is configured with background persistence (RDB snapshots), allowing the crawler queue to resume exactly where it left off in the event of worker or server restarts.
To evaluate the crawler's performance under maximum concurrency and eliminate network variance (ISP throttles, network latencies, robots.txt blocks), the project includes a synthetic web graph generator and a local HTTP server for reproducible benchmarking.
Generate a synthetic corpus of 2,000 linked HTML pages simulating a scale-free web graph (each containing 5β20 randomized outlinks and tech-themed text):
.venv\Scripts\python scripts\generate_mock_corpus.pyStart a lightweight local server on the host machine to serve the generated corpus at sub-millisecond local network speeds:
.venv\Scripts\python -m http.server 8090 --directory mock_corpus- In your browser at
http://localhost:8080/, add the seed:http://host.docker.internal:8090/page_0.html - Click Start Crawler in the UI.
- In your terminal, run the telemetry and search query benchmark suite:
.venv/Scripts/python benchmark.py
Below are the benchmark metrics obtained from a full crawl and index run of the 2,000-page synthetic technical corpus:
| Metric | Measured Telemetry | Optimization Rationale |
|---|---|---|
| Active Concurrent Workers | 10 Workers (Redis Heartbeats) | Improved worker resilience by recovering from transient database failures without interrupting active crawl workers. |
| Peak Crawl Throughput | ~300 pages/minute | Asynchronous worker execution coordinated through Redis-backed task queues. |
| Avg HTML Parse Speed | 9.27 ms | Reduced average HTML parsing time from ~3 s to ~9 ms by switching to the lxml parser and optimizing traversal. |
| Avg DB Indexing Time | 1,164 ms | Shifted CPU-heavy SentenceTransformer.encode calls to a thread pool (asyncio.to_thread) and limited PyTorch to 1 thread per process to eliminate CPU core thrashing. |
| Vocabulary Word Count | 1,985 distinct words | Implemented PostgreSQL ON CONFLICT DO NOTHING bulk inserts to prevent duplicate word key violations during concurrent indexes. |
| Median (P50) Search Latency | 31.99 ms | Hot-cached pre-calculated document embeddings and calculated cosine similarities using numpy matrix math. |
| Search Query Success Rate | 100.0% | Successfully executed 100 benchmark queries over the generated corpus. |


