feat(11-05): add the beachcomber search sync package and its Typesense engine
- Searchable, Engine, Gate and an init-time engine registry with the null engine - GORM callbacks installed through lagoon.OnDatabase register an after-commit sync that reloads the row and upserts or deletes its document - Gates run before any request: engine configured, database published, app Gate - hand-rolled net/http Typesense engine following the Scout wire contract - module README and root modules row
This commit is contained in:
91
modules/beachcomber/searchable.go
Normal file
91
modules/beachcomber/searchable.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package beachcomber
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Searchable is implemented (on the pointer receiver or the value) by a
|
||||
// model whose rows are kept in a search index. The GORM callbacks sync a
|
||||
// Searchable model after every create, update and delete.
|
||||
type Searchable interface {
|
||||
// SearchableAs is the index name without the search.prefix.
|
||||
SearchableAs() string
|
||||
// ToSearchableArray builds the document of the row. It runs after the
|
||||
// write committed, on a fresh copy reloaded by primary key, and may
|
||||
// query related rows through db. An error means nothing is indexed.
|
||||
ToSearchableArray(ctx context.Context, db *gorm.DB) (map[string]any, error)
|
||||
// ShouldBeSearchable reports whether the row belongs in the index. A
|
||||
// row that should not be searchable has its document removed.
|
||||
ShouldBeSearchable() bool
|
||||
}
|
||||
|
||||
// IndexSchemaProvider supplies the schema an engine creates the index with
|
||||
// when the index does not exist yet. For Typesense it is the collection
|
||||
// schema (fields, default_sorting_field); the engine adds the name.
|
||||
type IndexSchemaProvider interface {
|
||||
SearchIndexSchema() map[string]any
|
||||
}
|
||||
|
||||
// SearchKeyer replaces the document key, which defaults to the decimal
|
||||
// primary key. It is called on the written model, which may hold only its
|
||||
// primary key, so it must derive the key from the primary key.
|
||||
type SearchKeyer interface {
|
||||
SearchKey() string
|
||||
}
|
||||
|
||||
// Engine is a search index driver.
|
||||
type Engine interface {
|
||||
// Name is the search.driver value that selects the engine.
|
||||
Name() string
|
||||
// Configured reports whether the engine may send anything. When it is
|
||||
// false, sync is skipped without a request.
|
||||
Configured() bool
|
||||
// Upsert inserts or replaces docs in index, creating the index from
|
||||
// schema first when it does not exist.
|
||||
Upsert(ctx context.Context, index string, schema map[string]any, docs []map[string]any) error
|
||||
// Delete removes the documents with ids from index. A document that
|
||||
// is already gone is not an error.
|
||||
Delete(ctx context.Context, index string, ids []string) error
|
||||
// Flush drops the whole index. A missing index is not an error.
|
||||
Flush(ctx context.Context, index string) error
|
||||
// SearchIDs returns the ids of the documents matching q, in the
|
||||
// engine's order. They are candidates only: callers must re-check
|
||||
// every id against the database before exposing it.
|
||||
SearchIDs(ctx context.Context, index string, q Query) ([]string, error)
|
||||
}
|
||||
|
||||
// Query is an engine search request.
|
||||
type Query struct {
|
||||
// Q is the query text; empty means match everything ("*").
|
||||
Q string
|
||||
// QueryBy lists the fields Q is matched against.
|
||||
QueryBy []string
|
||||
// FilterBy is an engine filter expression, for example
|
||||
// "collection_id:=5".
|
||||
FilterBy string
|
||||
// SortBy is an engine sort expression, for example "created_at:desc".
|
||||
SortBy string
|
||||
// Page is 1-based; 0 leaves the engine default.
|
||||
Page int
|
||||
// PerPage is the page size; 0 leaves the engine default.
|
||||
PerPage int
|
||||
}
|
||||
|
||||
// Gate is the application kill-switch consulted before every sync. It
|
||||
// reads its setting through db; any error must count as off.
|
||||
type Gate interface {
|
||||
Enabled(ctx context.Context, db *gorm.DB) bool
|
||||
}
|
||||
|
||||
// GateFunc adapts a function to Gate.
|
||||
type GateFunc func(ctx context.Context, db *gorm.DB) bool
|
||||
|
||||
// Enabled calls f.
|
||||
func (f GateFunc) Enabled(ctx context.Context, db *gorm.DB) bool {
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
return f(ctx, db)
|
||||
}
|
||||
Reference in New Issue
Block a user