feat: search materials through beachcomber

- Material is a beachcomber Searchable (index jz_bm_materials) in the
  hand-written models/material_search.go; jz.bm requires summercms v0.6.4
- classes.MaterialIndexer asks Typesense for up to 250 candidates with
  body highlights; the app-wide Gate is on only for jz.bm-marked contexts
- GET /_bm/api/v1/materials takes q: candidates are re-gated, counted and
  paged in SQL in rank order, each row with at most one plain snippet
- search failure answers 503 Search is unavailable.; without q the list
  is unchanged
This commit is contained in:
Jakub Zych
2026-11-08 19:45:46 +01:00
parent f7c400756f
commit 4ffce6e6ac
18 changed files with 1315 additions and 36 deletions

View File

@@ -59,6 +59,9 @@ type Plugin struct {
// chatMember is the chat-group check of the realtime access guard; nil
// means classes.IsChatMember. Tests inject their own.
chatMember classes.MemberFunc
// materialIndex searches the materials index and keeps it current. Nil
// when Boot did not run: then search answers 503 and syncs are skipped.
materialIndex *classes.MaterialIndexer
}
func (p *Plugin) ID() string { return "jz.bm" }
@@ -74,7 +77,8 @@ func (p *Plugin) Register(app *backpack.App) error {
// Boot keeps the application, so route handlers and commands can reach its
// services, such as the database, when they run, and wires the realtime
// service and, when golem15.chat is active, the BM chat context. The
// service, the materials search and, when golem15.chat is active, the BM
// chat context. The
// management API uses the shared user.api_token middleware owned by
// golem15.user.
func (p *Plugin) Boot(app *backpack.App) error {
@@ -83,6 +87,9 @@ func (p *Plugin) Boot(app *backpack.App) error {
if err := p.wireRealtime(app); err != nil {
return err
}
if err := p.wireSearch(app); err != nil {
return err
}
if err := p.registerChat(app); err != nil {
return err
}