Files
summercms/modules/beachcomber/engines.go
Jakub Zych 7819c6746c feat(beachcomber): search highlights and field projection (quick-261108-pe4)
- Additive: Query.IncludeFields, ExcludeFields and Highlight; left empty,
  the Typesense request is the same as before.
- SearchResult.Hits holds one Hit per id, in step with IDs; SearchPage
  always fills it.
- Snippets are plain-text Segment runs split at private-use highlight tags,
  never HTML; MaxSnippetsPerArray is applied client-side.
- Optional HighlightSearcher capability, checked with SupportsHighlights
  (true for Typesense, false for null and third-party engines).
- README and docs: Highlights sections with the rule that a caller drops
  the hit of every id failing the SQL re-check before showing a snippet.

Targets framework v0.6.4 (additive, no breaking change).
2026-11-08 18:45:29 +01:00

82 lines
2.5 KiB
Go

package beachcomber
import (
"context"
"sort"
"sync"
"git.golem15.com/golem15/summercms/modules/backpack"
)
// NullEngine is the name of the built-in engine that indexes nothing.
const NullEngine = "null"
// EngineFactory builds an engine for an app.
type EngineFactory func(app *backpack.App) (Engine, error)
// engineTable is the init-time engine registry, like database/sql's: it is
// written only from package init functions and read when a Service is built.
var engineTable = struct {
mu sync.RWMutex
factories map[string]EngineFactory
}{factories: map[string]EngineFactory{}}
// RegisterEngine makes an engine available under name. Call it from the
// engine package's init function. A duplicate name, an empty name or a nil
// factory panics.
func RegisterEngine(name string, f EngineFactory) {
if name == "" {
panic("beachcomber: RegisterEngine with an empty name")
}
if f == nil {
panic("beachcomber: RegisterEngine " + name + " with a nil factory")
}
engineTable.mu.Lock()
defer engineTable.mu.Unlock()
if _, dup := engineTable.factories[name]; dup {
panic("beachcomber: RegisterEngine called twice for engine " + name)
}
engineTable.factories[name] = f
}
func engineFactory(name string) (EngineFactory, bool) {
engineTable.mu.RLock()
defer engineTable.mu.RUnlock()
f, ok := engineTable.factories[name]
return f, ok
}
func engineNames() []string {
engineTable.mu.RLock()
defer engineTable.mu.RUnlock()
names := make([]string, 0, len(engineTable.factories))
for n := range engineTable.factories {
names = append(names, n)
}
sort.Strings(names)
return names
}
func init() {
RegisterEngine(NullEngine, func(*backpack.App) (Engine, error) { return nullEngine{}, nil })
}
// nullEngine is never configured, so sync is skipped before it is called.
// Every method is a no-op; SearchIDs and SearchPage return no ids. It does
// not implement HighlightSearcher.
type nullEngine struct{}
func (nullEngine) Name() string { return NullEngine }
func (nullEngine) Configured() bool { return false }
func (nullEngine) Upsert(context.Context, string, map[string]any, []map[string]any) error {
return nil
}
func (nullEngine) Delete(context.Context, string, []string) error { return nil }
func (nullEngine) Flush(context.Context, string) error { return nil }
func (nullEngine) SearchIDs(context.Context, string, Query) ([]string, error) {
return []string{}, nil
}
func (nullEngine) SearchPage(context.Context, string, Query) (SearchResult, error) {
return SearchResult{IDs: []string{}, Hits: []Hit{}}, nil
}