Workload & Search Traffic
Interactive
Monthly End-User Searches
650,000 searches/mo
Keystroke Search Factor
3.8 calls / search
InstantSearch queries as you type. Real production average is 3.5 to 4.5 calls.
Total Catalog Records / Docs
350,000 records
Typesense Hosting Architecture
Algolia Plan Tier
MASSIVE SAVINGS: Typesense Slashes Search Bills by 98.9%! At 650k searches with 3.8 keystrokes (2.47M operations), Algolia bills $1,375/month. Self-hosting Typesense on Hetzner costs $14/month flat, saving you $16,332 every single year.
Algolia Monthly Invoice
$1,375
2,470,000 operations
Typesense Monthly Cost
$14
Unlimited search queries ($0/op)
Net Monthly Savings
$1,361
98.9% reduction
Annual Capital Saved
$16,332
Direct cash returned to runway
Cumulative Annual Spend: Algolia vs Typesense
CROSSOVER ADVANTAGE
Cost Structure Comparison
Audit Table
| Component Dimension | Algolia Search | Typesense (Hetzner / Cloud) | Architectural Difference |
|---|---|---|---|
| Search Operations Fee | $1,235.00 | $0.00 (Unlimited) | Algolia bills $0.50/1k requests; Typesense has 0 query taxes |
| Record Storage Fee | $140.00 | $0.00 (Included in RAM) | Algolia charges $0.40/1k records; Typesense bound only by RAM |
| Cluster & Compute Base | $0.00 | $14.00 / mo | Typesense runs on dedicated NVMe server (4 vCPU / 8GB) |
| Bandwidth & Egress | Included (Subject to AUP) | 20 TB Free on Hetzner | Zero egress penalties on typical e-commerce search payloads |
| TOTAL MONTHLY COMMITMENT | $1,375.00 | $14.00 | Saves $1,361/mo (98.9%) |
Migration Artifacts: Algolia to Typesense
Copyable Code
# docker-compose.yml - Production Typesense on Hetzner CPX31 ($14/mo)
version: '3.8'
services:
typesense:
image: typesense/typesense:27.1
container_name: typesense
restart: always
environment:
- TYPESENSE_DATA_DIR=/data
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY:-your_super_secure_admin_key_here}
- TYPESENSE_ENABLE_CORS=true
- TYPESENSE_HEALTH_CHECK_INTERVAL=60
volumes:
- /opt/typesense-data:/data
ports:
- "8108:8108"
ulimits:
nofile:
soft: 65535
hard: 65535
deploy:
resources:
limits:
memory: 7G
// Drop-in Replacement for Algolia InstantSearch in React / Next.js
import instantsearch from 'instantsearch.js';
import { searchBox, hits } from 'instantsearch.js/es/widgets';
import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter';
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: 'your_search_only_api_key', // Search-only scoped key
nodes: [
{
host: 'search.yourdomain.com', // Proxied via Cloudflare or direct
port: 443,
protocol: 'https',
},
],
cacheSearchResultsForSeconds: 120, // Client-side cache to save bandwidth
},
additionalSearchParameters: {
query_by: 'name,description,category,brand',
num_typos: 2,
},
});
const searchClient = typesenseInstantsearchAdapter.searchClient;
// Exactly identical to Algolia instantsearch initialization!
const search = instantsearch({
indexName: 'products',
searchClient,
});
search.addWidgets([
searchBox({ container: '#searchbox' }),
hits({
container: '#hits',
templates: {
item: (hit) => `<div><strong>${hit.name}</strong> - $${hit.price}</div>`,
},
}),
]);
search.start();
# sync_pg_to_typesense.py - Bulk index products from PostgreSQL into Typesense
import psycopg2
import typesense
client = typesense.Client({
'nodes': [{'host': 'localhost', 'port': '8108', 'protocol': 'http'}],
'api_key': 'your_admin_api_key',
'connection_timeout_seconds': 5
})
# Create Schema
schema = {
'name': 'products',
'fields': [
{'name': 'id', 'type': 'string'},
{'name': 'name', 'type': 'string'},
{'name': 'description', 'type': 'string', 'optional': True},
{'name': 'category', 'type': 'string', 'facet': True},
{'name': 'price', 'type': 'float', 'facet': True}
]
}
try:
client.collections['products'].delete()
except Exception:
pass
client.collections.create(schema)
# Stream rows from PostgreSQL
conn = psycopg2.connect("dbname=ecommerce user=postgres host=localhost password=secret")
cur = conn.cursor(name='product_cursor')
cur.execute("SELECT id::text, name, description, category, price FROM products WHERE is_active = true")
batch = []
for row in cur:
batch.append({
'id': row[0],
'name': row[1],
'description': row[2] or '',
'category': row[3] or 'General',
'price': float(row[4])
})
if len(batch) >= 1000:
client.collections['products'].documents.import_(batch, {'action': 'create'})
batch = []
if batch:
client.collections['products'].documents.import_(batch, {'action': 'create'})
print("[Success] All PostgreSQL products indexed into Typesense.")
#!/bin/bash
# backup_typesense_to_r2.sh - Nightly automated snapshot to Cloudflare R2
DATE=$(date +%Y-%m-%d_%H%M%S)
BACKUP_DIR="/opt/typesense-backups/$DATE"
mkdir -p "$BACKUP_DIR"
# 1. Trigger Typesense Snapshot API
curl -X POST "http://localhost:8108/operations/snapshot?snapshot_path=/data/snapshot-$DATE" -H "X-TYPESENSE-API-KEY: $TYPESENSE_API_KEY"
# 2. Archive and stream to Cloudflare R2 via rclone (Zero Egress!)
tar -czf "/tmp/typesense-backup-$DATE.tar.gz" -C /opt/typesense-data "snapshot-$DATE"
rclone copy "/tmp/typesense-backup-$DATE.tar.gz" "r2:search-backups/typesense/"
# 3. Clean local temporary snapshot
rm -rf "/opt/typesense-data/snapshot-$DATE" "/tmp/typesense-backup-$DATE.tar.gz"
echo "[Backup Complete] Streamed to Cloudflare R2: typesense-backup-$DATE.tar.gz"