omnisearch

package
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: UNKNOWN not legal advice Imports: 0 Imported by: 0

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

View Source
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.

View Source
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.

View Source
const OmnisearchViewType components.ViewType = "omnisearch-view"

OmnisearchViewType tags the body view. Local because nothing in components renders this feature.

Variables

View Source
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 Filter

type Filter struct {
	Key   string
	Value string
}

Filter is one `key:value` qualifier.

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

func New(deps Deps) *Handler

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

Handle is the entry point for every `$search` URL. A nil view return means the body has already been written.

func (*Handler) Search

func (h *Handler) Search(ctx context.Context, q *Query) (groups []Group, unknown string)

Search answers a query. At most one selector runs: they name different subjects, so running all of them would turn one keystroke into a dozen queries. Without a selector the query falls to the discovery search.

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

type Limiter interface {
	AllowRequest(r *http.Request) bool
}

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

func ParseQuery(raw string) (*Query, error)

ParseQuery validates and tokenises a raw `q` value. Each qualifier splits on its FIRST colon only: a value may legitimately contain one.

func (*Query) Get

func (q *Query) Get(key string) (string, bool)

Get returns the first value for a qualifier, and whether it was present.

func (*Query) HasBareWord

func (q *Query) HasBareWord(word string) bool

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 Scope

type Scope string

Scope says what a selector needs in order to answer.

const (
	// ScopePackage needs a package path, from the URL or from `in:`.
	ScopePackage Scope = "package"

	// ScopeGlobal answers from the term alone.
	ScopeGlobal Scope = "global"
)

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.

type Source

type Source string

Source travels to the template because the distinction is not cosmetic: chain results are consensus data, indexer results are one operator's best effort.

const (
	// SourceChain marks a result answered by the RPC node.
	SourceChain Source = "chain"

	// SourceIndexer is never consensus data.
	SourceIndexer Source = "indexer"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL