feat(beachcomber): search highlights and field projection (quick-261108-pe4)
- Additive: Query.IncludeFields, ExcludeFields and Highlight; left empty, the Typesense request is the same as before. - SearchResult.Hits holds one Hit per id, in step with IDs; SearchPage always fills it. - Snippets are plain-text Segment runs split at private-use highlight tags, never HTML; MaxSnippetsPerArray is applied client-side. - Optional HighlightSearcher capability, checked with SupportsHighlights (true for Typesense, false for null and third-party engines). - README and docs: Highlights sections with the rule that a caller drops the hit of every id failing the SQL re-check before showing a snippet. Targets framework v0.6.4 (additive, no breaking change).
This commit is contained in:
@@ -75,6 +75,32 @@ type Query struct {
|
||||
Page int
|
||||
// PerPage is the page size; 0 leaves the engine default.
|
||||
PerPage int
|
||||
// IncludeFields and ExcludeFields only limit which document fields the
|
||||
// engine sends back: a field left out of the answer stays searchable
|
||||
// and highlightable. Empty keeps the engine default (every stored
|
||||
// field). The document key is always returned, because the ids are the
|
||||
// result. Engines that cannot project ignore them.
|
||||
IncludeFields []string
|
||||
ExcludeFields []string
|
||||
// Highlight asks for snippets of the matches in SearchResult.Hits. Nil
|
||||
// asks for no highlights and keeps the request as it was. Engines
|
||||
// without HighlightSearcher ignore it.
|
||||
Highlight *Highlight
|
||||
}
|
||||
|
||||
// Highlight configures the snippets an engine returns for a Query.
|
||||
// Snippets are always cut around the matches; there is deliberately no
|
||||
// option to highlight a whole field.
|
||||
type Highlight struct {
|
||||
// Fields are the fields to highlight. Empty is the engine default,
|
||||
// which is the QueryBy fields for Typesense.
|
||||
Fields []string
|
||||
// AffixTokens is the number of tokens of context on each side of a
|
||||
// match; 0 is the engine default.
|
||||
AffixTokens int
|
||||
// MaxSnippetsPerArray is the most snippets kept for one array field;
|
||||
// 0 means no cap.
|
||||
MaxSnippetsPerArray int
|
||||
}
|
||||
|
||||
// SearchResult is one page of candidate ids and the number of documents
|
||||
@@ -83,6 +109,36 @@ type Query struct {
|
||||
type SearchResult struct {
|
||||
IDs []string
|
||||
Found int
|
||||
// Hits has one entry per id, in the same order as IDs
|
||||
// (Hits[i].ID == IDs[i]). An engine called directly may leave it nil;
|
||||
// SearchPage always fills it. Like the ids, hits are candidates: drop
|
||||
// the Hit of every id the SQL re-check rejects before showing a
|
||||
// snippet.
|
||||
Hits []Hit
|
||||
}
|
||||
|
||||
// Hit is the engine's answer for one id beyond the id itself.
|
||||
type Hit struct {
|
||||
ID string
|
||||
// Highlights is nil when the hit has none.
|
||||
Highlights []Snippet
|
||||
}
|
||||
|
||||
// Snippet is one highlighted excerpt of a field.
|
||||
type Snippet struct {
|
||||
Field string
|
||||
// ArrayIndex is the element index for an array field, and -1 for a
|
||||
// non-array field or when the engine did not say which element.
|
||||
ArrayIndex int
|
||||
Segments []Segment
|
||||
}
|
||||
|
||||
// Segment is a run of a snippet that either matched the query or not.
|
||||
// Text is plain stored text, verbatim, never HTML or markup. It can
|
||||
// contain < or &, so the caller escapes it when rendering.
|
||||
type Segment struct {
|
||||
Text string
|
||||
Match bool
|
||||
}
|
||||
|
||||
// PageSearcher is implemented by an engine that can report how many
|
||||
@@ -92,18 +148,51 @@ type PageSearcher interface {
|
||||
SearchPage(ctx context.Context, index string, q Query) (SearchResult, error)
|
||||
}
|
||||
|
||||
// HighlightSearcher is implemented by an engine that can return snippets
|
||||
// of the matches. It is optional so that existing engines stay valid. An
|
||||
// engine that implements it and answers true fills Hit.Highlights for a
|
||||
// Query with Highlight; ask through SupportsHighlights.
|
||||
type HighlightSearcher interface {
|
||||
PageSearcher
|
||||
SupportsHighlights() bool
|
||||
}
|
||||
|
||||
// SupportsHighlights reports whether e fills Hit.Highlights: true only when
|
||||
// e implements HighlightSearcher and its SupportsHighlights answers true. A
|
||||
// nil e is false. When it is false, an app builds its own snippets from the
|
||||
// rows it loads.
|
||||
func SupportsHighlights(e Engine) bool {
|
||||
hs, ok := e.(HighlightSearcher)
|
||||
return ok && hs.SupportsHighlights()
|
||||
}
|
||||
|
||||
// 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.
|
||||
// of ids returned. On success Hits is always in step with IDs: when the
|
||||
// engine left it nil, it is one Hit without highlights per id (an empty,
|
||||
// non-nil slice for zero ids); hits the engine filled are kept as they are.
|
||||
func SearchPage(ctx context.Context, e Engine, index string, q Query) (SearchResult, error) {
|
||||
var res SearchResult
|
||||
if ps, ok := e.(PageSearcher); ok {
|
||||
return ps.SearchPage(ctx, index, q)
|
||||
r, err := ps.SearchPage(ctx, index, q)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
res = r
|
||||
} else {
|
||||
ids, err := e.SearchIDs(ctx, index, q)
|
||||
if err != nil {
|
||||
return SearchResult{}, err
|
||||
}
|
||||
res = SearchResult{IDs: ids, Found: len(ids)}
|
||||
}
|
||||
ids, err := e.SearchIDs(ctx, index, q)
|
||||
if err != nil {
|
||||
return SearchResult{}, err
|
||||
if res.Hits == nil {
|
||||
res.Hits = make([]Hit, len(res.IDs))
|
||||
for i, id := range res.IDs {
|
||||
res.Hits[i] = Hit{ID: id}
|
||||
}
|
||||
}
|
||||
return SearchResult{IDs: ids, Found: len(ids)}, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// IndexDropper is implemented by an engine that can tell whether the index
|
||||
|
||||
Reference in New Issue
Block a user