v1.0 The Elasticsearch alternative without the price tag

Search engines
shouldn't cost
a fortune.

Flatseek is a disk-first trigram index that works on serverless. No JVM. No heap tuning. No cluster. Point it at a CSV,JSON,XLSX and query through a dashboard, REST API, or Python — from one file to billions of rows.

$ curl -fsSL flatseek.io/install.sh | sh
or pip install flatseek

macOS · Linux · Python 3.11+  ·  View source on GitHub

~80%
Lower infrastructure cost vs Elasticsearch
~40%
Smaller index footprint on disk
0
JVM heap parameters to tune
0/mo
Only storage cost to run a full index
Cost & Footprint

Stop paying for hardware
you don't need.

Elasticsearch's JVM heap and cluster requirements add up fast. Flatseek stores everything on disk, memory-mapped — serverless is enough.

Comparison
Trigram, disk-first vs JVM cluster
Open source
Flatseek New
Industry default
Elasticsearch
Infrastructure
On demand serverless
Dedicated cluster, 64GB RAM
Long-running node
Query and exit
Required 24/7
JVM heap tuning
None
Per-deployment
Index size on disk
Compact trigram
Inverted, 2–3× larger
Dashboard included
Built-in Flatlens
Separate Kibana license
Memory residency
OS-managed mmap
Heap-resident
Bottom line
Only cost you storage.
You pay for cluster + heap + ops.
Two surfaces

Pick what fits
your workflow.

Flatlens for analysts who want quick explore in local. CLI & Python for engineers shipping it into production.

Dashboard

Flatlens — for day-to-day CSV work

Open a CSV in your browser. Search it, chart it, map it. Built for analysts.

  • Drag-drop CSV, JSON, JSONL, XLS & XLSX
  • Visual search with a filter builder
  • Bar, line, donut & pie aggregations
  • Interactive Leaflet map with clustering
  • Encrypt indices with a passphrase
Read the Flatlens guide
CLI & API

CLI & Python — for production scale

Handle billion-row indices. Automate pipelines. Embed in your services. Parallel workers, resume on interrupt, REST API for your own frontend.

  • flatseek build · serve · search · stats · plan
  • Parallel multi-worker indexing with resume
  • Cross-index joins on shared fields
  • Python client — API mode or direct mode
  • Natural language chat via Ollama
Read the CLI & API docs
Capabilities

Everything you need
from a search engine.

No surprises. The features you'd expect from Elasticsearch, minus the cluster.

Multi-format ingest

CSV, JSON, JSONL, XLS, XLSX. Auto-detects delimiters and column types on import.

Disk-native queries

Memory-mapped trigram index. Resident memory stays low, regardless of index size.

Full-text search

Wildcards, fuzzy, phrase. AND / OR / NOT with grouping. Works on every text field.

On-disk aggregations

Terms, stats, date histograms, cardinality. Computed without loading docs into RAM.

ChaCha20-Poly1305 encryption

Passphrase-protected indices with PBKDF2-HMAC-SHA256 key derivation.

Parallel builds with resume

Multi-worker indexing with auto-planning. Resume interrupted builds with ETA display.

Live CLI

A million rows indexed
under 5 minutes

flatseek  —  ~/projects/locations  —  zsh
$flatseek build flatdata/data/locations.csv -o ./locations -w 8
── Pre-classify ──────────────────────────────────────
Classify locations.csv:
'id'KEYWORD (80%)
'name'TEXT (100%)
'country'KEYWORD (100%)
'lat'FLOAT (100%)
'lng'FLOAT (100%)
'category'KEYWORD (100%)
'population'FLOAT (80%)
Counting rows in 1 file(s)...
locations.csv: ~1,000,000 rows (52.9 MB)
Single file — splitting into 8 byte-range chunks (O(1) seek)...
Plan written: ./locations/build_plan.json
── Launching 8/8 workers ─────────────────────────────
Worker 0 pid 52067 log → worker_0.log
Worker 1 pid 52068 log → worker_1.log
Worker 2 pid 52069 log → worker_2.log
Worker 3-7 …
Wrowsstateckptencodediskmemckpt_r
✓ 0126,754flush221.2s2.7s43MB99%
✓ 1124,757flush221.1s3.0s43MB99%
✓ 2124,742flush220.8s2.8s44MB99%
✓ 3124,757flush221.0s3.5s44MB99%
✓ 4124,759flush221.4s3.1s44MB99%
✓ 5124,750flush221.3s3.3s44MB99%
✓ 6124,733flush220.8s3.0s44MB99%
✓ 7124,748flush221.9s3.2s46MB99%
100.0% 1,000,000 / 1,000,000 · 4m42s elapsed
── Workers done (4m42s) ─────────────────────────────
Worker 0 [done] 126,754 new rows (total: 126,754 docs)
Worker 1 [done] 124,757 new rows (total: 424,757 docs)
Worker 2 [done] 124,742 new rows (total: 724,742 docs)
Worker 3 [done] 124,757 new rows (total: 1,024,757 docs)
Worker 4-7 …
── Merging stats ─────────────────────────────────────
Merged stats: 1,000,000 docs, 22,636,800 entries, 485.7 MB
All 8 workers completed.
$# Single-keyword search across 1M docs
$flatseek search ./locations "jakarta" -n 3
Query: jakarta
Found: 23,714 match(es) (page 1, showing 3)
--- 1 ---
DOC {'_id': 55, 'id': '56', 'name': 'Jakarta', 'country': 'ID', 'lat': '-6.217089', 'lng': '106.833582', 'category': 'tourist', 'population': '121262'}
--- 2 ---
DOC {'_id': 70, 'id': '71', 'name': 'Jakarta', 'country': 'ID', 'lat': '-6.225444', 'lng': '106.808682', 'category': 'regional', 'population': '94715'}
--- 3 ---
DOC {'_id': 95, 'id': '96', 'name': 'Jakarta', 'country': 'ID', 'lat': '-6.228413', 'lng': '106.816618', 'category': 'tourist', 'population': '686130'}
$# Field filter + boolean AND
$flatseek search ./locations "country:ID AND category:tourist" -n 2
Query: country:ID AND category:tourist
Found: 52,515 match(es) (page 1, showing 2)
DOC {'_id': 9, 'id': '10', 'name': 'Bogor', 'country': 'ID', 'lat': '-6.679548', 'lng': '106.800171', 'category': 'tourist', 'population': '251629'}
DOC {'_id': 13, 'id': '14', 'name': 'Tangerang', 'country': 'ID', 'lat': '-6.22116', 'lng': '106.643498', 'category': 'tourist', 'population': '814544'}
$# Wildcard prefix + numeric range
$flatseek search ./locations 'name:bogor* AND population:[100000 TO 500000]' -n 2
Query: name:bogor* AND population:[100000 TO 500000]
Found: 572 match(es) (page 1, showing 2)
DOC {'_id': 1459, 'id': '1460', 'name': 'Bogor', 'country': 'ID', 'category': 'major', 'population': '251629'}
DOC {'_id': 2532, 'id': '2533', 'name': 'Bogor', 'country': 'ID', 'category': 'industrial', 'population': '187340'}
$flatseek stats ./locations
Docs: 1,000,000
Index: 465.0 MB (524288 files)
Doc store: 20.8 MB
Total: 485.7 MB
Columns:
category KEYWORD
country KEYWORD
id KEYWORD
lat FLOAT
lng FLOAT
name TEXT
population FLOAT
$# Plan a build without executing
$flatseek plan ./flatdata/data/locations.csv -o ./locations
Counting rows in 1 file(s)...
locations.csv: ~1,000,000 rows (52.9 MB)
Single file — splitting into 4 byte-range chunks (O(1) seek)...
Plan written: ./locations/build_plan.json
Worker 0: bytes [45…13,878,994] (13 MB) ~250,000 rows
Worker 1: bytes [13,878,994…27,757,919] (13 MB) ~250,000 rows
Worker 2: bytes [27,757,919…41,636,876] (13 MB) ~250,000 rows
Worker 3: bytes [41,636,876…55,515,777] (13 MB) ~250,000 rows
$flatseek serve -d ./locations -p 8000
2026-04-22 09:58:01 [INFO] flatseek.api.main: Flatlens dashboard mounted at /dashboard
INFO: Will watch for changes in these directories: ['./locations']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Starting Flatseek API + Dashboard on 0.0.0.0:8000
Data directory: ./locations
API: http://localhost:8000
Dashboard: http://localhost:8000/dashboard
Opening dashboard in browser...
Press Ctrl+C to stop
2026-04-22 09:58:02 [INFO] Started server process [52729]
INFO: Application startup complete.
127.0.0.1:50021 - "GET /dashboard HTTP/1.1" 302 Found
127.0.0.1:50021 - "GET /dashboard/ HTTP/1.1" 200 OK
127.0.0.1:50029 - "GET /locations/_search?q=jakarta HTTP/1.1" 200 OK
$# Run aggregations via REST — no doc loaded into RAM
$curl -X POST http://localhost:8000/locations/_aggregate \
-H "Content-Type: application/json" -d '{
"query": "*",
"aggs": {
"by_country": { "terms": { "field": "country", "size": 5 } },
"by_category": { "terms": { "field": "category", "size": 5 } },
"pop_stats": { "stats": { "field": "population" } }
}
}'
{ "took": 42, "hits": { "total": 1000000 }, "aggregations": { "by_country": { "buckets": [ { "key": "ID", "doc_count": 211284 }, { "key": "NG", "doc_count": 198557 }, { "key": "BR", "doc_count": 187902 }, { "key": "IN", "doc_count": 176443 }, { "key": "US", "doc_count": 112985 } ] }, "by_category": { "buckets": [ { "key": "tourist", "doc_count": 252108 }, { "key": "industrial", "doc_count": 249772 }, { "key": "regional", "doc_count": 249654 }, { "key": "major", "doc_count": 248466 } ] }, "pop_stats": { "count": 1000000, "min": 512, "max": 9998247, "avg": 2503641.7, "sum": 2503641723 } } }
$python
Python 3.11.6 (main, Oct 2 2023, 13:45:54) on darwin
›››from flatseek import Flatseek
›››qe = Flatseek("./locations") # direct mode — no server needed
›››r = qe.search(q="name:jakarta AND country:ID", size=2)
›››print(f"total = {r.total:,}")
total = 23,714
›››for doc in r.docs:
... print(doc["name"], doc["category"], doc["population"])
Jakarta tourist 121262
Jakarta regional 94715
›››agg = qe.aggregate(q="*", aggs={
... "by_category": {"terms": {"field": "category", "size": 5}}
...})
›››[(b["key"], b["doc_count"]) for b in agg.aggs["by_category"]["buckets"]]
[('tourist', 252108), ('industrial', 249772),
('regional', 249654), ('major', 248466)]
Get started

Stop over-engineering
your search.

One install command. Your data stays on your machine.