Files
summercms/modules/beachcomber/searchable.go
Jakub Zych 3ae49bb6f4 feat(12-01): report the search engine's found count and field weights
- optional beachcomber.PageSearcher returns a page of candidate ids plus
  the engine's found count; beachcomber.SearchPage falls back to
  SearchIDs for engines without it, so Engine is unchanged
- Query.QueryByWeights is sent to Typesense as query_by_weights; a
  mismatched weight list or a page above typesense.MaxPerPage (250) is
  refused before any request
2026-10-02 11:37:01 +02:00

127 lines
4.7 KiB
Go

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
// QueryByWeights ranks the QueryBy fields, one weight per field in the
// same order (Typesense query_by_weights). Empty leaves the engine
// default; a length different from QueryBy is an error.
QueryByWeights []int
// 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
}
// SearchResult is one page of candidate ids and the number of documents
// the engine found for the query. Both are candidates only: callers re-gate
// the ids, and recount any total they expose, in SQL.
type SearchResult struct {
IDs []string
Found int
}
// PageSearcher is implemented by an engine that can report how many
// documents matched besides the page of ids. It is optional so that
// existing Engine implementations stay valid; use SearchPage to call it.
type PageSearcher interface {
SearchPage(ctx context.Context, index string, q Query) (SearchResult, error)
}
// SearchPage asks e for one page of q: through PageSearcher when e
// implements it, otherwise through SearchIDs with Found set to the number
// of ids returned.
func SearchPage(ctx context.Context, e Engine, index string, q Query) (SearchResult, error) {
if ps, ok := e.(PageSearcher); ok {
return ps.SearchPage(ctx, index, q)
}
ids, err := e.SearchIDs(ctx, index, q)
if err != nil {
return SearchResult{}, err
}
return SearchResult{IDs: ids, Found: len(ids)}, nil
}
// Gate is the application kill-switch consulted before every sync. It
// reads its setting through db, a clean session on the write's connection
// (inside a caller's plain transaction, a savepoint of it); 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)
}