Files
arsam 872b407f1e Import documentation: README.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:06:56 +03:30

9.9 KiB

Axon Search API

Axon is a private, self-hosted AI search API for Linux servers. It aggregates search results from Brave Search, DuckDuckGo, and Serper.dev, optionally scrapes full page content, reranks results with a local BGE cross-encoder, and can synthesize answers or research reports through any OpenAI-compatible LLM endpoint.

Features

  • POST /search for multi-source search with optional scraping, reranking, and answer synthesis
  • POST /extract for clean content extraction from one or more URLs
  • POST /research for multi-query research reports
  • POST /crawl for same-site traversal with extraction
  • POST /map for same-site URL discovery
  • GET /health for cache and reranker status
  • GET /metrics for lightweight request counters and latency
  • POST /ingest and POST /index/search for private local document retrieval
  • /research/jobs, /crawl/jobs, /map/jobs, and GET /jobs/{id} for async long-running work
  • /tavily/* compatibility routes for easier migration
  • POST /validate/citations for basic citation coverage checks
  • API key auth via X-API-Key
  • Per-key/IP rate limiting
  • Redis cache with in-memory fallback
  • CORS enabled for all origins
  • SSRF protection for URL scraping and extraction

Setup

Axon requires Python 3.11 or newer.

python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env

Edit .env with any API keys you want to enable. At least DuckDuckGo works without a key, but Brave and Serper improve coverage and reliability.

Start the server:

uvicorn axon.main:app --host 0.0.0.0 --port 8000

The reranker model is loaded at startup. The first run may download the configured HuggingFace model and can take longer than later starts.

Configuration

Variable Required Default Purpose
BRAVE_API_KEY No empty Enables Brave Search results
SERPER_API_KEY No empty Enables Serper.dev fallback results
REDIS_URL No redis://localhost:6379 Redis cache URL; falls back to memory if unavailable
API_KEYS No empty Comma-separated accepted API keys; empty disables auth
LITELLM_BASE_URL No http://localhost:4000 OpenAI-compatible chat completions base URL
LITELLM_API_KEY No empty Bearer token for synthesis requests
SYNTHESIS_MODEL No claude-sonnet-4-6 Model sent to the LLM endpoint
RERANKER_MODEL No BAAI/bge-reranker-v2-m3 Sentence Transformers cross-encoder model
PORT No 8000 Preferred server port
MAX_CONCURRENT_SCRAPES No 10 Concurrent scrape limit for advanced search
SCRAPE_TIMEOUT No 5 Per-URL scrape timeout in seconds
EXTRACT_TIMEOUT No 10 Per-URL extraction timeout in seconds
RATE_LIMIT_PER_MINUTE No 60 Per API key or client IP limit; 0 disables
CRAWL_MAX_PAGES No 50 Server-side cap for crawl/map pages
CRAWL_MAX_DEPTH No 3 Server-side cap for crawl/map depth
INDEX_DB_PATH No axon_index.sqlite3 SQLite FTS database path for private indexed docs
ENABLE_BROWSER_FALLBACK No false Enables optional Playwright rendering fallback when installed

Authentication

If API_KEYS is unset or empty, Axon runs in development mode and does not require authentication.

If API_KEYS is set, send one configured key with each protected request:

curl -H "X-API-Key: your-secret-key" http://localhost:8000/health

/health, /docs, /openapi.json, and /redoc are exempt.

Endpoints

Health

curl http://localhost:8000/health

Example response:

{
  "status": "ok",
  "redis": "in-memory fallback",
  "reranker": "loaded"
}

Metrics

curl http://localhost:8000/metrics

Example response:

{
  "requests_total": 42,
  "requests_by_path": {
    "/search": 10,
    "/extract": 3
  },
  "errors_total": 0,
  "average_latency_ms": 1234.5
}

Basic search uses search API snippets and reranks them:

curl -X POST http://localhost:8000/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "query": "latest PostgreSQL indexing improvements",
    "max_results": 10,
    "search_depth": "basic",
    "include_answer": false
  }'

Advanced search scrapes result pages before reranking:

curl -X POST http://localhost:8000/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "query": "private AI search APIs",
    "max_results": 10,
    "search_depth": "advanced",
    "include_answer": true,
    "include_raw_content": false,
    "include_private": true,
    "domains_blacklist": ["reddit.com"],
    "topic": "general",
    "time_range": "month"
  }'

Extract

curl -X POST http://localhost:8000/extract \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "urls": [
      "https://example.com/article"
    ],
    "include_images": false
  }'

Each URL is processed independently. Failed URLs return success: false with an error while the rest of the request continues.

Research

curl -X POST http://localhost:8000/research \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "query": "state of open source vector databases",
    "depth": "deep",
    "search_depth": "basic",
    "max_results_per_query": 5,
    "output_format": "markdown"
  }'

depth: "basic" generates 3 sub-queries. depth: "deep" generates 5 sub-queries. Sources are deduplicated by URL, and the final report is synthesized from the combined results. Use search_depth: "basic" for faster research, or advanced when scraped page content is worth the extra latency.

For long-running work, create a background job:

curl -X POST http://localhost:8000/research/jobs \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{"query": "OpenAI", "search_depth": "basic"}'

curl http://localhost:8000/jobs/<job_id> \
  -H "X-API-Key: your-secret-key"

Crawl

curl -X POST http://localhost:8000/crawl \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "url": "https://docs.example.com",
    "max_depth": 1,
    "max_pages": 20,
    "include_raw_content": false,
    "instructions": "python sdk"
  }'

/crawl stays on the same hostname as the start URL, extracts each matching page, and returns discovered links. It is intentionally bounded by CRAWL_MAX_PAGES and CRAWL_MAX_DEPTH.

Map

curl -X POST http://localhost:8000/map \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "url": "https://docs.example.com",
    "max_depth": 2,
    "max_pages": 50,
    "instructions": "api reference"
  }'

/map returns same-site URLs without extracting full content.

Private Index

Ingest private documents:

curl -X POST http://localhost:8000/ingest \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "documents": [
      {
        "id": "runbook-1",
        "title": "Production Runbook",
        "content": "Internal deployment and rollback notes.",
        "url": "private://runbook-1"
      }
    ]
  }'

Search only the private index:

curl -X POST http://localhost:8000/index/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{"query": "rollback notes"}'

Normal /search includes private index hits by default. Set "include_private": false to use only web sources.

Tavily-Compatible Routes

Axon exposes compatibility routes under /tavily:

curl -X POST http://localhost:8000/tavily/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{"query": "OpenAI", "max_results": 5}'

Available compatibility routes:

  • /tavily/search
  • /tavily/extract
  • /tavily/crawl
  • /tavily/map

Citation Validation

curl -X POST http://localhost:8000/validate/citations \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "report": "OpenAI was founded in 2015 [1].",
    "sources": [
      {"title": "Source", "url": "https://example.com", "content": "OpenAI was founded in 2015."}
    ]
  }'

Search Source Logic

Axon fetches Brave and DuckDuckGo concurrently. Serper.dev is called only when Brave returns fewer than 5 results. All results are merged and deduplicated by URL before filtering, scraping, reranking, and response truncation.

If all enabled sources return no results, /search returns HTTP 503. Individual source failures are logged and treated as empty result sets.

DuckDuckGo uses the current ddgs package when installed and falls back to duckduckgo-search for compatibility.

Caching

  • Search responses are cached for 10 minutes.
  • Successful extracted URL content is cached for 1 hour.
  • Redis is used when REDIS_URL is configured and reachable.
  • If Redis is unavailable, Axon logs a warning and uses in-memory cache for the current process.

Performance Notes

Advanced search is slower than basic search because it fetches and extracts each result page. Tune MAX_CONCURRENT_SCRAPES and SCRAPE_TIMEOUT for your server and network limits.

The reranker model is loaded once during FastAPI startup and reused for requests. Use a smaller cross-encoder model if startup time, memory use, or CPU latency is too high for your deployment.

Reranking is serialized through a process-local lock and capped to a small number of chunks per document. This keeps the self-hosted service stable on modest servers, at the cost of some recall on very long pages.

Optional browser rendering requires installing Playwright and browser assets separately, then setting ENABLE_BROWSER_FALLBACK=true.

Development

Run tests:

source venv/bin/activate
python -m pytest tests/ -v

Run a focused test file:

python -m pytest tests/test_search_router.py -v