aRustyDev / aRustyDev/mdbook-htmx

docs(adr): ADR-0005: Server-Side Search Index

Open
#13 0 comments 0 reactions 1 assignee Claimed by @aRustyDev View on GitHub
documentation
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 %}


{% 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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.