search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add search architecture overview

explains current FTS5 setup, what's coupled, and future
Elasticsearch path if scaling is ever needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

zzstoatzz 74ddd037 4bb1bb6c

+123
+123
docs/search-architecture.md
··· 1 + # search architecture 2 + 3 + current state, rationale, and future options. 4 + 5 + ## current: SQLite FTS5 6 + 7 + we use SQLite's built-in full-text search (FTS5) via Turso. 8 + 9 + ### why FTS5 works for now 10 + 11 + - **scale**: ~3500 documents. FTS5 handles this trivially. 12 + - **latency**: 10-50ms for search queries. fine for our use case. 13 + - **cost**: $0. included with Turso free tier. 14 + - **ops**: zero. no separate service to run. 15 + - **simplicity**: one database for everything (docs, FTS, vectors, cache). 16 + 17 + ### how it works 18 + 19 + ``` 20 + user query: "crypto-casino" 21 + 22 + buildFtsQuery(): "crypto OR casino*" 23 + 24 + FTS5 MATCH query with BM25 ranking 25 + 26 + results with snippet() 27 + ``` 28 + 29 + key decisions: 30 + - **OR between terms** for better recall (deliberate, see commit 35ad4b5) 31 + - **prefix match on last word** for type-ahead feel 32 + - **unicode61 tokenizer** splits on non-alphanumeric (we match this in buildFtsQuery) 33 + 34 + ### what's coupled to FTS5 35 + 36 + all in `backend/src/search.zig`: 37 + 38 + | component | FTS5-specific | 39 + |-----------|---------------| 40 + | 10 query definitions | `MATCH`, `snippet()`, `ORDER BY rank` | 41 + | `buildFtsQuery()` | constructs FTS5 syntax | 42 + | schema | `documents_fts`, `publications_fts` virtual tables | 43 + 44 + ### what's already decoupled 45 + 46 + - result types (`SearchResultJson`, `Doc`, `Pub`) 47 + - similarity search (uses `vector_distance_cos`, not FTS5) 48 + - caching logic 49 + - HTTP layer (server.zig just calls `search()`) 50 + 51 + ### known limitations 52 + 53 + - **no typo tolerance**: "leafet" won't find "leaflet" 54 + - **no relevance tuning**: can't boost title vs content 55 + - **single writer**: SQLite write lock 56 + - **no horizontal scaling**: single database 57 + 58 + these aren't problems at current scale. 59 + 60 + ## future: if we need to scale 61 + 62 + ### when to consider switching 63 + 64 + - search latency consistently >100ms 65 + - write contention from indexing 66 + - need typo tolerance or better relevance 67 + - millions of documents 68 + 69 + ### recommended: Elasticsearch 70 + 71 + Elasticsearch is the battle-tested choice for production search: 72 + 73 + - proven at massive scale (Wikipedia, GitHub, Stack Overflow) 74 + - rich query DSL, analyzers, aggregations 75 + - typo tolerance via fuzzy matching 76 + - horizontal scaling built-in 77 + - extensive tooling and community 78 + 79 + trade-offs: 80 + - operational complexity (JVM, cluster management) 81 + - resource hungry (~2GB+ RAM minimum) 82 + - cost: $50-500/month depending on scale 83 + 84 + ### alternatives considered 85 + 86 + **Meilisearch/Typesense**: simpler, lighter, great defaults. good for straightforward search but less proven at scale. would work fine for this use case but Elasticsearch has more headroom. 87 + 88 + **Algolia**: fully managed, excellent but expensive. makes sense if you want zero ops. 89 + 90 + **PostgreSQL full-text**: if already on Postgres. not as good as FTS5 or Elasticsearch but one less system. 91 + 92 + ### migration path 93 + 94 + 1. keep Turso as source of truth 95 + 2. add Elasticsearch as search index 96 + 3. sync documents to ES on write (async) 97 + 4. point `/search` at Elasticsearch 98 + 5. keep `/similar` on Turso (vector search) 99 + 100 + the `search()` function would change from SQL queries to ES client calls. result types stay the same. HTTP layer unchanged. 101 + 102 + estimated effort: 1-2 days to swap search backend. 103 + 104 + ### vector search scaling 105 + 106 + similarity search currently uses brute-force `vector_distance_cos` with caching. at scale: 107 + 108 + - **Elasticsearch**: has vector search (dense_vector + kNN) 109 + - **dedicated vector DB**: Qdrant, Pinecone, Weaviate 110 + - **pgvector**: if on Postgres 111 + 112 + could consolidate text + vector in Elasticsearch, or keep them separate. 113 + 114 + ## summary 115 + 116 + | scale | recommendation | 117 + |-------|----------------| 118 + | <10k docs | keep FTS5 (current) | 119 + | 10k-100k docs | still probably fine, monitor latency | 120 + | 100k+ docs | consider Elasticsearch | 121 + | millions + sub-ms latency | Elasticsearch cluster + caching layer | 122 + 123 + we're in the "keep FTS5" zone. the code is structured to swap later if needed.