Documentation
¶
Overview ¶
Package omnisearch owns every `$search` URL: the server-rendered results page and the JSON the header omnibar consumes.
A query is free text plus any number of `key:value` qualifiers. One selector answers it, and names where the answer came from — the chain, or a configured indexer. Indexer-backed selectors exist only when one is configured. See gno.land/adr/pr6231_gnoweb_indexer.md.
Index ¶
Constants ¶
const ( // MaxQueryLen fits a couple of qualifiers plus a bech32 address. MaxQueryLen = 256 // Past a handful, qualifiers stop narrowing and start being free work. MaxFilters = 8 // Below MinTermLen a query matches most of the chain. MinTermLen = 2 // MaxResults caps one group: relevant, not exhaustive. MaxResults = 20 )
Input bounds. A search box is attacker-controlled and every resolver turns its terms into a chain or indexer query, so it is screened before any fetch. Mirrors ADR-003 §Resource bounds.
const ( FilterAuthor = "author" FilterIn = "in" FilterIs = "is" )
Narrowing qualifiers refine a search without choosing what is searched. Listed so an unrecognised one is reported as a typo, not ignored.
const OmnisearchViewType components.ViewType = "omnisearch-view"
OmnisearchViewType tags the body view. Local because nothing in components renders this feature.
Variables ¶
var ( // ErrQueryTooLong reports a `q` over MaxQueryLen. ErrQueryTooLong = errors.New("query too long") // ErrTooManyFilters reports more than MaxFilters qualifiers. ErrTooManyFilters = errors.New("too many search qualifiers") // ErrQueryInvalid rejects rather than strips: the value round-trips into // HTML and log lines, where a CR or LF is someone else's bug. ErrQueryInvalid = errors.New("query contains control characters") )
Functions ¶
func NewPageView ¶
func NewPageView(data SearchData) *components.View
NewPageView wraps the results page so callers in gnoweb's Get pipeline can compose it inside IndexLayout via the standard (status, *components.View) return shape.
Types ¶
type ClientAdapter ¶
type ClientAdapter interface {
Realm(ctx context.Context, path, args string) ([]byte, error)
Doc(ctx context.Context, path string, height int64) (*doc.JSONDocumentation, error)
ListFiles(ctx context.Context, path string, height int64) ([]string, error)
}
ClientAdapter is the subset of gnoweb.ClientAdapter this feature consumes, declared locally to avoid a back import (gnoweb imports omnisearch).
type Deps ¶
type Deps struct {
// Client answers the chain-backed selectors. Required.
Client ClientAdapter
// Directory lists realm and package paths for the discovery search.
// Required.
Directory Directory
// Indexer answers the indexer-backed selectors. Optional: nil means no
// indexer is configured and those selectors do not exist.
Indexer Indexer
// Domain is the chain's domain (e.g. "gno.land"), used to turn a gnoweb
// path into the fully qualified package path the chain indexes under.
Domain string
// Limiter bounds the per-IP request rate. Optional; nil disables the
// check.
Limiter Limiter
Logger *slog.Logger
}
Deps is a struct of interfaces so each field is independently mockable.
type Directory ¶
type Directory interface {
// Paths returns the realm and package paths, and whether the node capped
// the listing. Truncation travels with the answer because a silent cap
// is how a search comes to say "no such realm" about one that exists.
Paths(ctx context.Context) (realms, packages []string, truncated bool, err error)
}
Directory lists the chain's paths. Shaped after gnoweb's RealmDirectory so the wire-in passes the instance /search.json already uses, singleflight and semaphore included — qpaths is the most expensive RPC gnoweb makes.
type Group ¶
type Group struct {
Label string
Source Source
Results []Result
// Err renders as a failure rather than being dropped: "could not ask" is
// not "nothing matched".
Err error
// Truncated marks a partial answer: more may exist than Results holds.
// Notice says why, in the reader's terms.
Truncated bool
Notice string
}
Group is one answered subject: several for a discovery search, one when a selector chose the subject.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler owns every `$search` URL.
func New ¶
New validates required deps and returns a Handler. The selector table is built once: which selectors exist is a property of the deployment.
func (*Handler) Handle ¶
func (h *Handler) Handle(ctx context.Context, w http.ResponseWriter, r *http.Request, u *weburl.GnoURL) (int, *components.View)
Handle is the entry point for every `$search` URL. A nil view return means the body has already been written.
type Indexer ¶
type Indexer interface {
LatestBlockHeight(ctx context.Context) (int, error)
TxByHash(ctx context.Context, hash string) (*indexer.Tx, error)
RecentByPackage(ctx context.Context, pkgPath string, limit int) ([]indexer.Tx, error)
RecentByAddress(ctx context.Context, addr string, limit int) ([]indexer.Tx, error)
Deploys(ctx context.Context, pkgPath string, limit int) ([]indexer.Tx, error)
SourceContains(ctx context.Context, text, author string, limit int) ([]indexer.Tx, error)
DeploysImporting(ctx context.Context, pkgPath string, limit int) ([]indexer.Tx, error)
Block(ctx context.Context, height int) (*indexer.Block, error)
URL() string
}
Indexer is the subset of *indexer.Client this feature consumes. A nil Indexer is the feature switch: New registers the indexer-backed selectors only when one is present.
type IndexerStatus ¶
type IndexerStatus struct {
URL string
LastBlock int
// Err makes the footer say the freshness is unknown rather than omit it.
Err error
}
IndexerStatus is the provenance footer: indexer results are not consensus data, so the reader gets the endpoint and how far behind it is.
type Limiter ¶
Limiter is the admission check applied before any fetch. It takes the request, not an address: which address to trust depends on which proxies to trust, and that rule belongs with the limiter. Nil disables the check.
type Query ¶
type Query struct {
// Raw is the validated input, as typed; Text is what is left once the
// qualifiers are removed.
Raw string
Text string
Filters []Filter
// PkgPath is gnoweb-relative ("/r/demo/boards"), the form Doc/ListFiles
// take and every href is built from. ChainPath is the same package
// domain-qualified, the form the chain and the indexer use. Not
// interchangeable.
PkgPath string
ChainPath string
// contains filtered or unexported fields
}
Query is a parsed search input: any number of `key:value` qualifiers plus the free text left over.
func ParseQuery ¶
ParseQuery validates and tokenises a raw `q` value. Each qualifier splits on its FIRST colon only: a value may legitimately contain one.
func (*Query) HasBareWord ¶
HasBareWord matches a no-argument selector typed as a bare word.
type Result ¶
type Result struct {
Title string
Detail string
// Href is typed so escaping is enforced by construction. Empty when the
// row has no destination.
Href template.URL
// Tags are short badges. Provenance is not among them — it belongs to
// the group, and Group.Source carries it once.
Tags []string
}
Result is one row in the results list.
type SearchData ¶
type SearchData struct {
Query string
// PkgPath is the scoped package, gnoweb-relative; empty when the URL
// names none.
PkgPath string
// WholeChainHref, set on a scoped discovery search, repeats it without
// the scope.
WholeChainHref string
// FormAction is explicit rather than `action=""`, which would re-submit
// the webargs already in the path and grow the URL on every search.
FormAction string
Groups []Group
// Selectors is the honest inventory of what this deployment can answer.
Selectors []*Selector
// UnknownFilter names a qualifier that is neither a selector nor a
// narrowing keyword, so the page can say so instead of showing nothing.
UnknownFilter string
// Indexer is set only when an indexer-backed group ran; nil drops the
// provenance footer entirely.
Indexer *IndexerStatus
}
SearchData is the render payload for templates/page.html — field names must match the template.
func (SearchData) HasErrors ¶
func (d SearchData) HasErrors() bool
HasErrors reports a group that could not answer: "Nothing matched." under "Could not answer" would contradict it.
func (SearchData) HasResults ¶
func (d SearchData) HasResults() bool
HasResults tells "nothing matched" from "nothing was asked".
type Selector ¶
type Selector struct {
// Name is the qualifier key, e.g. "tx" in `tx:9f2a`.
Name string
// Hint is the form shown in the omnibar's suggestion list.
Hint string
Label string
Scope Scope
// Source is stamped by register from the table the selector is in, not
// by the literal. See Handler.register.
Source Source
// Bare marks a selector typed without a colon (`imports`, `activity`).
Bare bool
// MinTerm raises MinTermLen where the term's selectivity sets the cost.
MinTerm int
// PageOnly keeps a fan-out selector off the per-keystroke JSON path.
PageOnly bool
// contains filtered or unexported fields
}
Selector chooses what a query searches. Exactly one runs per query — running all of them would turn one keystroke into a dozen queries.