textsearch

package
v12.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

Documentation

Overview

Package textsearch defines an interface for a search index management structure

Index

Constants

View Source
const (
	OperationIndex  = "index"
	OperationDelete = "delete"
	OperationWipe   = "wipe"
	OperationSearch = "search"
)

The operations a text search backend performs, and the values the operation attribute takes. They are named here rather than per backend so a dashboard written against one backend reads the other.

View Source
const DefaultSearchLimit = 25

DefaultSearchLimit is the page size used when a SearchRequest does not name one. It is stated here rather than left to each backend because the backends disagree: Elasticsearch defaults to 10 hits and Algolia to 20, so a caller that omitted the limit got a different page depending on which one it was talking to.

View Source
const (
	// QueryKeySearch is the query param key to find search queries in requests.
	QueryKeySearch = "q"
)

Variables

View Source
var (
	// ErrInvalidCursor is returned when a cursor cannot be decoded, or was issued
	// by a different backend than the one being asked to resume it.
	//
	// The second case is the common one in practice. A cursor is opaque, so it
	// travels in whatever field the API spells pagination with, and that field is
	// usually shared with the database-backed listings — which means a cursor
	// from a SQL page, or one left over from a backend swap, arrives here looking
	// exactly like a search cursor. Refusing it is the point: the alternative is
	// interpreting a position that happens to parse.
	ErrInvalidCursor = platformerrors.New("invalid search cursor")

	// ErrEmptyQueryProvided is returned when a search is attempted with no query
	// text. It is not a match-all, for the reason SearchRequest.Query gives: the
	// backends disagree about what an empty query means, and none of them means
	// "return the entire index".
	ErrEmptyQueryProvided = platformerrors.New("empty search query provided")

	// ErrResultWindowExceeded is returned when pagination reached the depth a
	// backend will serve — Elasticsearch's index.max_result_window, Algolia's
	// paginationLimitedTo. The cursor was valid and the page it named is past the
	// end of what can be paged to.
	//
	// It is an error rather than an empty last page because those are different
	// facts. An empty page says "that was everything", which a caller is entitled
	// to treat as authoritative; this says the result set continues and this
	// index will not walk to it, which a caller answers by narrowing the query,
	// not by paging again.
	ErrResultWindowExceeded = platformerrors.New("search result window exceeded")
)

The refusals a text index makes about the request rather than about itself.

They live beside the interface, not in the backend that raises them, because they are answers a caller has to act on and a caller does not know which backend is installed. A service that matched elasticsearch.ErrEmptyQuery would stop matching the day it moved to Algolia — the refusal identical, the sentinel a different value — so the branch has to be written once per backend, in application code, for a distinction the interface says does not exist.

The corollary is that a backend raises the one that describes its refusal, not one it invents. A backend with no result-window ceiling simply never returns ErrResultWindowExceeded.

Both transports map all three: see errors/grpc and errors/http.

Functions

func DecodeCursor

func DecodeCursor(backend string, c Cursor) (int, error)

DecodeCursor reads a cursor issued by the named backend, returning the position it resumes at. A zero cursor decodes to 0 with no error, so a backend need not special-case the first page.

It is exported for backend implementations, not for callers of Search.

func EffectiveLimit

func EffectiveLimit(requested, ceiling int) int

EffectiveLimit resolves a requested limit against the default and the backend's own ceiling. It exists so the three backends cannot disagree about what an unset limit means, which is how they ended up capping at 10 and 20.

Types

type Cursor

type Cursor string

Cursor is an opaque resumption token. Callers pass it back verbatim and must not construct, parse, or persist meaning from one: the encoding belongs to the backend that issued it and is expected to change.

It is opaque precisely because the backends do not agree on what resumption means. Both currently encode an offset, but Elasticsearch's eventual move to search_after carries the previous hit's sort values instead, which is not an offset and cannot be one. Spelling the interface as an offset would have made that a breaking change; spelling it as a token makes it an implementation detail.

func EncodeCursor

func EncodeCursor(backend string, position int) (Cursor, error)

EncodeCursor builds an opaque cursor for the given backend.

position is whatever resuming means to that backend — a document offset for Elasticsearch, a page number for Algolia. Nothing outside the issuing backend interprets it, which is the point of the token being opaque.

It is exported for backend implementations, not for callers of Search.

func (Cursor) IsZero

func (c Cursor) IsZero() bool

IsZero reports whether the cursor is unset, meaning "start from the beginning" on the way in and "no more results" on the way out.

type Index

type Index[T any] interface {
	IndexSearcher[T]
	IndexManager
}

Index is our wrapper interface for a text search index.

type IndexManager

type IndexManager interface {
	Index(ctx context.Context, id string, value any) error
	Delete(ctx context.Context, id string) (err error)
	Wipe(ctx context.Context) error
}

IndexManager is our wrapper interface for a text search index.

type IndexSearcher

type IndexSearcher[T any] interface {
	Search(ctx context.Context, req SearchRequest) (*SearchResults[T], error)
}

type Instruments

type Instruments struct {
	// contains filtered or unexported fields
}

Instruments is what a text search backend records, and lives here rather than in each backend because the alternative is two copies of the same six registrations that drift the first time one of them gains a seventh.

Three instruments, each meaning one thing, keyed by operation: how many calls there were, how many of them failed, and how long they took. A per-operation counter apiece would answer the same questions and make "the error rate of searches" a division across four names.

func NewInstruments

func NewInstruments(backend, indexName string, metricsProvider metrics.Provider) (*Instruments, error)

NewInstruments builds the instruments for one backend's one index. A nil provider records nothing, which is how a caller asks for no metrics.

func (*Instruments) Record

func (i *Instruments) Record(ctx context.Context, operation string, started time.Time, err error)

Record notes one completed operation: that it happened, how long it took, and whether it failed.

Every operation is counted, failed ones included, so the failure counter is a numerator over a denominator that is actually the same population — the ratio is the error rate, without a second subtraction.

type SearchRequest

type SearchRequest struct {
	// Query is the text to search for. An empty query is an error rather than a
	// match-all: every backend here treats it differently, and the one thing
	// none of them means by it is "return the entire index".
	Query string

	// Cursor resumes a previous search. The zero value starts at the beginning.
	// A cursor is only meaningful to the backend that issued it.
	Cursor Cursor

	// Limit is the maximum number of hits to return. Zero means
	// DefaultSearchLimit; backends may cap it lower.
	Limit int
}

SearchRequest is one page of one query.

It is a struct rather than positional parameters because the pagination fields are the kind that get added to over time — a filter, a sort, a scoring hint — and each addition would otherwise break every implementation.

type SearchResults

type SearchResults[T any] struct {
	// NextCursor resumes after the last hit in Hits. It is empty when the
	// result set is exhausted, which is how a caller knows to stop — not a
	// short page, since a backend may return fewer hits than requested and
	// still have more.
	NextCursor Cursor

	// Hits are the documents matched, in the backend's relevance order.
	Hits []*T
}

SearchResults is one page of hits, plus the cursor for the next one.

func (*SearchResults[T]) Done

func (r *SearchResults[T]) Done() bool

Done reports whether this is the last page.

Directories

Path Synopsis
Package algolia provides an interface-compatible wrapper around the algolia indexer
Package algolia provides an interface-compatible wrapper around the algolia indexer
Package textsearchcfg selects and builds a text search index from configuration: Elasticsearch, Algolia, or noop.
Package textsearchcfg selects and builds a text search index from configuration: Elasticsearch, Algolia, or noop.
Package elasticsearch provides an interface-compatible wrapper around the elasticsearch indexer
Package elasticsearch provides an interface-compatible wrapper around the elasticsearch indexer
Package textsearchmock provides moq-generated mocks for the search/text package.
Package textsearchmock provides moq-generated mocks for the search/text package.
Package noop is the textsearch.Index for a service with no search cluster: Index, Delete, and Wipe all succeed and keep nothing, and Search returns zero hits.
Package noop is the textsearch.Index for a service with no search cluster: Index, Delete, and Wipe all succeed and keep nothing, and Search returns zero hits.

Jump to

Keyboard shortcuts

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