aRustyDev / aRustyDev/mdbook-htmx
docs(adr): ADR-0005: Server-Side Search Index
- Dominant language
- Rust
- Stars
- 0
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
# ADR-0005: Server-Side Search Index
## Status
Accepted
## Context
MDBook's default HTML renderer includes client-side search powered by elasticlunr.js. The search index is loaded as JSON and executed in the browser.
For an HTMX-based architecture, we need to decide where search execution happens.
## Decision Drivers
1. **HTMX Philosophy** - Server returns HTML, not JSON
2. **Performance** - Large books have large indexes
3. **Authorization** - Search results should respect access control
4. **Simplicity** - Minimize client-side JavaScript
## Options Considered
### Option A: Client-Side Search (like mdbook-html)
Ship elasticlunr.js and JSON index to browser.
```html
const index = elasticlunr.Index.load(searchIndex);
function search(query) {
const results = index.search(query);
document.getElementById('results').innerHTML = renderResults(results);
}
```
**Pros:**
- No server required
- Instant results (after index load)
- Works offline
**Cons:**
- Large index download (100KB+ for big books)
- Slow initial load
- JavaScript required
- No auth filtering (all content in index)
- Against HTMX philosophy
### Option B: Server-Side Search with JSON Response
Server searches, returns JSON, client renders.
```html
```
Server returns:
```json
{"results": [{"title": "Chapter 1", "url": "/docs/ch1"}]}
```
**Pros:**
- Index stays on server
- Can filter by auth
**Cons:**
- Requires client-side rendering
- Not hypermedia (JSON, not HTML)
- More JavaScript
### Option C: Server-Side Search with HTML Response (Recommended)
Server searches, returns HTML fragment.
```html
```
Server returns:
```html
```
**Pros:**
- True hypermedia (HTML response)
- No client-side rendering logic
- Authorization-aware (filter results)
- Index never sent to client
- Works with minimal JS (just HTMX)
**Cons:**
- Requires server
- Network latency per search
## Decision
**Generate server-side search index; output HTML search results template.**
### Index Format
Default: `search-index.json`
```json
{
"config": {
"heading_split_level": 3
},
"documents": [
{
"path": "/docs/chapter-1",
"title": "Getting Started",
"body": "Full text content...",
"headings": [
{"level": 2, "text": "Installation", "anchor": "#installation"},
{"level": 2, "text": "Configuration", "anchor": "#configuration"}
],
"auth": {
"access": "public"
}
}
]
}
```
Optional: `search-index.sqlite` for large books (using FTS5).
### Search Partial Template
```html
{% if results %}
-
{{ result.title }}
{{ result.excerpt | safe }}
{% for result in results %}
{% endfor %}
{% else %}
No results found for "{{ query }}"
{% endif %}
```
### Server Implementation
```typescript
// Cloudflare Worker example
import Fuse from 'fuse.js';
import searchIndex from './search-index.json';
const fuse = new Fuse(searchIndex.documents, {
keys: ['title', 'body', 'headings.text'],
includeMatches: true,
threshold: 0.3,
});
app.get('/docs/search', async (c) => {
const query = c.req.query('q');
const user = await getUser(c.req);
let results = fuse.search(query);
// Filter by auth
results = results.filter(r =>
r.item.auth.access === 'public' ||
user?.roles.includes(r.item.auth.roles)
);
return c.html(await render('partials/search-results.html', {
query,
results: results.slice(0, 10)
}));
});
```
## Consequences
### Positive
- True hypermedia pattern
- Authorization-aware search
- No large index download
- Minimal client JavaScript
- Consistent with HTMX philosophy
### Negative
- Requires server for search
- Network latency per query
- Server must implement search logic
### Mitigation
- Debounce search input (300ms)
- Loading indicator during search
- Provide reference implementations (Workers, Deno, Node)
- Optional SQLite FTS5 for large books
## Configuration
```toml
[output.htmx.search]
enabled = true
index-format = "json" # "json" | "sqlite"
heading-split-level = 3 # Index headings up to H3
include-body = true # Include full text
max-excerpt-length = 200 # Snippet length
```
## Related ADRs
- [ADR-0009: Audience Scopes](./0009-audience-scopes.md) - Generates scope-filtered search indexes (e.g., `search-index.developers.json`)
- [ADR-0021: Search Index Format Evolution](./0021-search-index-format-evolution.md) - Versioning and migration strategy for the search index format
## References
- [HTMX Active Search Pattern](https://htmx.org/examples/active-search/)
- [Fuse.js - Lightweight Fuzzy Search](https://fusejs.io/)
- [SQLite FTS5](https://www.sqlite.org/fts5.html)
Contributor guide
Assessment
This issue has not been assessed yet.