Search
The battery/search package defines a pluggable full-text search interface.
The framework ships one in-memory backend; production deployments swap in
a real engine (Postgres FTS, Bleve, Meilisearch, etc.) behind the same
Backend interface.
Quickstart
import "github.com/DonaldMurillo/gofastr/battery/search"index := search.NewMemory()// Index a document._ = index.Index(ctx, search.Document{ ID: "post-1", Type: "posts", Text: "GoFastr framework release notes", Fields: map[string]any{"author": "carol", "tags": []string{"go"}},})// Query it.results, err := index.Search(ctx, search.Query{ Text: "framework", Type: "posts", Limit: 10,})
The blog example wires this into GET /posts/search?q=.... See
examples/blog/main.go.
Types
Document
| Field | Type | Notes |
|---|---|---|
ID | string | Required. Unique per backend. |
Type | string | Optional. Filterable namespace ("posts", etc.). |
Text | string | Free text body the engine tokenises. |
Fields | map[string]any | Optional structured fields returned with hits. |
Query
| Field | Type | Notes |
|---|---|---|
Text | string | Query text. Empty string matches all documents. |
Type | string | Optional. Restricts results to one Document.Type. |
Limit | int | Max hits. 0 means no limit (engine default). |
Offset | int | Skip first N hits — useful for pagination. |
Result
type Result struct { Document Document Score float64}
Score is engine-specific. The memory backend uses a naive term-frequency
score; a real engine returns BM25 or similar. Don't compare scores
across backends.
The Backend interface
type Backend interface { Index(ctx context.Context, doc Document) error Delete(ctx context.Context, id string) error Search(ctx context.Context, query Query) ([]Result, error)}
Anything that implements all three methods is a valid backend. There
is no registration step — wire your backend wherever you would have
used search.NewMemory().
The in-memory backend
search.NewMemory() returns a goroutine-safe in-process store. It is
appropriate for tests, single-binary demos, and small read-only sites.
Documents are stored in a map, so:
- Restarting the process drops the index.
- Memory usage is
O(total text size). - Search is
O(n)over the corpus.
The implementation is in battery/search/memory.go — read it before
using it in anything user-facing.
Battery wrapper
search.NewBattery(backend) wraps any Backend in a
framework.Battery so the index participates in the App's
dependency-resolved lifecycle. This lets other batteries declare
"search" as a dependency, guaranteeing the index is available before
their own Init runs.
idx := search.NewMemory()app.Batteries.Register(search.NewBattery(idx))// Retrieve the backend from within another battery:b, _ := app.Batteries.Get("search")sb := b.(*search.Battery)results, _ := sb.Backend().Search(ctx, search.Query{Text: "..."})
search.Memory has no background goroutine, so Battery implements
framework.Battery only (no BatteryLifecycle). If a future
production backend starts a background goroutine, implement io.Closer
on that type and the Battery wrapper's OnStop will call Close()
automatically.
Common mistakes
- Indexing inside a hook without context. Pass the request context
toIndexso cancellations propagate. Don't usecontext.Background()
from inside a request-scoped hook — you'll leak goroutines on client
disconnect. - Expecting
Typeto be required. It isn't. Searches without a
Typefilter scan all documents. SetTypeconsistently when you
index, or be ready for cross-entity results. - Treating the memory backend as durable. It isn't. Wire a real
backend before relying on the index surviving restart.