A federated search system for Cooklang recipes that allows decentralized publishing and centralized discovery through RSS/Atom feeds.
- 🔍 Unified search with powerful query syntax powered by Tantivy
- 📡 RSS/Atom feed crawler with automatic updates
- 🏷️ Advanced filtering by tags, ingredients, time, difficulty, and more
- 🌐 Web UI for browsing and searching recipes
- 💻 CLI tools for searching, downloading, and publishing recipes
- 🔄 Background scheduler for automated feed crawling
- 🛡️ Rate limiting to protect API endpoints from abuse
- 🐳 Docker support for easy deployment
# Clone the repository
git clone <repository-url>
cd federation
# Start the server
docker-compose up -d
# Access the web UI
open http://localhost:3000- Rust 1.75 or later
- SQLite 3
# Clone the repository
git clone <repository-url>
cd federation
# Copy environment variables
cp .env.example .env
# Download Tailwind CSS CLI
./scripts/download-tailwind.sh
# Start development server (with Tailwind watch mode)
./scripts/dev.shThe server will be available at http://localhost:3001
# Build Tailwind CSS once
./tailwindcss -i ./styles/input.css -o ./src/web/static/css/output.css
# Run database migrations
cargo run -- migrate
# Start the server
cargo run -- serveThe federation search supports powerful query syntax powered by Tantivy's QueryParser:
# Search all fields
breakfast
# Search specific field
tags:breakfast
title:pasta
ingredients:tomato
difficulty:easy# Boolean operators
pasta AND tags:italian
breakfast OR brunch
# Exclusion
chocolate -tags:dessert
# Range queries
total_time:[0 TO 30] # 30 minutes or less
servings:[4 TO 8] # Serves 4-8 people
# Complex combinations
chocolate tags:dessert difficulty:easy
pasta AND tags:italian AND total_time:[0 TO 30]Use quotes for multi-word field values:
tags:"quick breakfast"
title:"chocolate chip cookies"title- Recipe titlesummary- Recipe descriptioninstructions- Cooking instructionsingredients- Ingredient listtags- Recipe tagsdifficulty- Difficulty level (easy, medium, hard)servings- Number of servingstotal_time- Total cooking time in minutesfile_path- Source file path (for GitHub recipes)
# Basic search
cargo run -- search "chocolate cookies"
# Field-specific search
cargo run -- search "tags:breakfast"
# Complex query
cargo run -- search "pasta AND tags:italian AND total_time:[0 TO 30]"cargo run -- download 123 --output ./recipes# Generate an Atom feed from .cook files
cargo run -- publish --input ./my-recipes --output feed.xmlDetect and store the locale of recipes that don't have one yet (author-declared
locale: metadata wins, otherwise the language is detected from the recipe text).
Touched recipes are re-indexed automatically.
# Tag only recipes that don't have a locale yet
cargo run -- backfill-locales
# Recompute the locale for every recipe, including ones already tagged
cargo run -- backfill-locales --forceGET /health- Health checkGET /ready- Readiness checkGET /api/stats- System statistics
GET /api/search?q=<query>- Search recipes with unified query syntaxlocale(optional) - Filter to a single language, e.g.locale=de. Filtering by a base language (en) also matches its regional variants (en-US).- Examples:
/api/search?q=breakfast- Basic search/api/search?q=tags:breakfast- Field-specific search/api/search?q=pasta%20AND%20tags:italian- Boolean search/api/search?q=total_time:[0%20TO%2030]- Range search/api/search?q=breakfast&locale=fr- Search restricted to French recipes
GET /api/recipes/:id- Get recipe details, includinglocale(e.g."de","en-US") andlocale_source("declared"if set via a Cooklanglocale:key, or"detected"if inferred from the recipe text)GET /api/recipes/:id/download- Download .cook file
GET /api/feeds- List all feedsPOST /api/feeds- Register a new feedGET /api/feeds/:id- Get feed detailsDELETE /api/feeds/:id- Remove a feed
Environment variables (see .env.example):
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
Database connection string | sqlite:./data/federation.db |
HOST |
Server host | 0.0.0.0 |
PORT |
Server port | 3000 |
EXTERNAL_URL |
External URL for CLI | http://localhost:3000 |
API_RATE_LIMIT |
API requests per second | 100 |
CRAWLER_INTERVAL |
Seconds between feed updates | 3600 |
MAX_FEED_SIZE |
Maximum feed size in bytes | 5242880 (5MB) |
MAX_RECIPE_SIZE |
Maximum recipe size in bytes | 1048576 (1MB) |
RATE_LIMIT |
Crawler requests per second per domain | 1 |
INDEX_PATH |
Search index directory | ./data/index |
RUST_LOG |
Logging level | info,federation=debug |
This release adds a locale field to the Tantivy search schema (used to tag
and filter recipes by language). Tantivy pins field ids to the schema stored
on disk, so a search index built before this change is incompatible — the
server now refuses to start against a mismatched index, with an error
telling you what to do.
Before deploying this version, delete the existing index and rebuild it:
rm -rf data/index # or your configured INDEX_PATH
federation backfill-localesbackfill-locales runs pending database migrations and re-indexes every
recipe it touches, so this single command rebuilds the search index and
backfills locales in one step. Run it before starting serve again.
To build the project for production:
# Build everything (CSS + Rust binary)
./scripts/build.shThis will:
- Build Tailwind CSS with minification
- Build the Rust binary in release mode
The output will be:
- Binary:
./target/release/federation - Minified CSS:
./src/web/static/css/output.css
To run in production:
# Set environment variables
export DATABASE_URL="sqlite:./data/federation.db"
export PORT=3000
# Run the server
./target/release/federation servecargo testcargo clippy -- -D warningsMigrations are located in the migrations/ directory and are automatically applied on server startup.
- Web Framework: Axum with Tokio async runtime
- Database: SQLite (PostgreSQL compatible)
- Search Engine: Tantivy full-text search
- Feed Parsing: feed-rs for RSS/Atom
- Recipe Parsing: cooklang-rs
- Templates: Askama with Tailwind CSS
- CLI: Clap for command-line interface
- Rate Limiting: tower-governor for API protection
For production use, it's recommended to deploy this service behind a reverse proxy (nginx, Caddy, Traefik, etc.) that:
- Terminates TLS/SSL
- Sets proper
X-Forwarded-Forheaders for accurate rate limiting - Provides additional DDoS protection
- Handles load balancing if running multiple instances
Rate limiting works best when proper IP information is available via reverse proxy headers.
To make your recipes discoverable:
- Create
.cookfiles in a directory - Generate an Atom feed:
cargo run -- publish --input ./recipes --output feed.xml
- Host the feed and .cook files at a public URL
- Add your feed to a federation server:
curl -X POST http://localhost:3000/api/feeds \ -H "Content-Type: application/json" \ -d '{"url": "https://your-site.com/feed.xml"}'
See LICENSE file for details.
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.