From f7d496389e40b87c97f76785354c504fb63f5f5c Mon Sep 17 00:00:00 2001 From: Caleb Champlin Date: Sat, 17 Oct 2020 13:42:23 -0600 Subject: [PATCH] Add support for highlights --- highlight.go | 341 ++++++++++++++++++++++++++++++++++++++++++++++ highlight_test.go | 93 +++++++++++++ search.go | 23 +++- 3 files changed, 451 insertions(+), 6 deletions(-) create mode 100644 highlight.go create mode 100644 highlight_test.go diff --git a/highlight.go b/highlight.go new file mode 100644 index 0000000..9c076bc --- /dev/null +++ b/highlight.go @@ -0,0 +1,341 @@ +package esquery + +import ( + "github.com/fatih/structs" +) + +// Map returns a map representation of the highlight; implementing the +// Mappable interface. +func (q *QueryHighlight) Map() map[string]interface{} { + results := structs.Map(q.params) + if q.highlightQuery != nil { + results["query"] = q.highlightQuery.Map() + } + if q.fields != nil && len(q.fields) > 0 { + fields := make(map[string]interface{}) + for k, v := range q.fields { + fields[k] = v.Map() + } + results["fields"] = fields + } + return results +} + +type QueryHighlight struct { + highlightQuery Mappable `structs:"highlight_query,omitempty"` + fields map[string]*QueryHighlight `structs:"fields"` + params highlighParams +} + +type highlighParams struct { + PreTags []string `structs:"pre_tags,omitempty"` + PostTags []string `structs:"post_tags,omitempty"` + + FragmentSize uint16 `structs:"fragment_size,omitempty"` + NumberOfFragments uint16 `structs:"number_of_fragments,omitempty"` + Type HighlightType `structs:"type,string,omitempty"` + BoundaryChars string `structs:"boundary_chars,omitempty"` + BoundaryMaxScan uint16 `structs:"boundary_max_scan,omitempty"` + BoundaryScanner HighlightBoundaryScanner `structs:"boundary_scanner,string,omitempty"` + BoundaryScannerLocale string `structs:"boundary_scanner_locale,omitempty"` + Encoder HighlightEncoder `structs:"encoder,string,omitempty"` + ForceSource *bool `structs:"force_source,omitempty"` + Fragmenter HighlightFragmenter `structs:"fragmenter,string,omitempty"` + FragmentOffset uint16 `structs:"fragment_offset,omitempty"` + MatchedFields []string `structs:"matched_fields,omitempty"` + NoMatchSize uint16 `structs:"no_match_size,omitempty"` + Order HighlightOrder `structs:"order,string,omitempty"` + PhraseLimit uint16 `structs:"phrase_limit,omitempty"` + RequireFieldMatch *bool `structs:"require_field_match,omitempty"` + TagsSchema HighlightTagsSchema `structs:"tags_schema,string,omitempty"` +} + +// Highlight creates a new "query" of type "highlight" +func Highlight() *QueryHighlight { + return newHighlight() +} + +func newHighlight() *QueryHighlight { + return &QueryHighlight{ + fields: make(map[string]*QueryHighlight), + params: highlighParams{}, + } +} + +// PreTags sets the highlight query's pre_tags ignore unmapped field +func (q *QueryHighlight) PreTags(s ...string) *QueryHighlight { + q.params.PreTags = append(q.params.PreTags,s...) + return q +} + +// PostTags sets the highlight query's post_tags ignore unmapped field +func (q *QueryHighlight) PostTags(s ...string) *QueryHighlight { + q.params.PostTags = append(q.params.PostTags,s...) + return q +} + +// Field sets an entry the highlight query's fields +func (q *QueryHighlight) Field(name string, h ...*QueryHighlight) *QueryHighlight { + var fld *QueryHighlight + if len(h) > 0 { + fld = h[len(h)-1] + } else { + fld = &QueryHighlight{} + } + q.fields[name] = fld + return q +} + +// Fields sets all entries for the highlight query's fields +func (q *QueryHighlight) Fields(h map[string]*QueryHighlight) *QueryHighlight { + q.fields = h + return q +} + +// FragmentSize sets the highlight query's fragment_size ignore unmapped field +func (q *QueryHighlight) FragmentSize(i uint16) *QueryHighlight { + q.params.FragmentSize = i + return q +} + +// NumberOfFragments sets the highlight query's number_of_fragments ignore unmapped field +func (q *QueryHighlight) NumberOfFragments(i uint16) *QueryHighlight { + q.params.NumberOfFragments = i + return q +} + +// Type sets the highlight query's type ignore unmapped field +func (q *QueryHighlight) Type(t HighlightType) *QueryHighlight { + q.params.Type = t + return q +} + +// BoundaryChars sets the highlight query's boundary_chars ignore unmapped field +func (q *QueryHighlight) BoundaryChars(s string) *QueryHighlight { + q.params.BoundaryChars = s + return q +} + +// BoundaryMaxScan sets the highlight query's boundary_max_scan ignore unmapped field +func (q *QueryHighlight) BoundaryMaxScan(i uint16) *QueryHighlight { + q.params.BoundaryMaxScan = i + return q +} + +// BoundaryScanner sets the highlight query's boundary_scanner ignore unmapped field +func (q *QueryHighlight) BoundaryScanner(t HighlightBoundaryScanner) *QueryHighlight { + q.params.BoundaryScanner = t + return q +} + +// BoundaryScannerLocale sets the highlight query's boundary_scanner_locale ignore unmapped field +func (q *QueryHighlight) BoundaryScannerLocale(l string) *QueryHighlight { + q.params.BoundaryScannerLocale = l + return q +} + +// Encoder sets the highlight query's encoder ignore unmapped field +func (q *QueryHighlight) Encoder(e HighlightEncoder) *QueryHighlight { + q.params.Encoder = e + return q +} + +// ForceSource sets the highlight query's force_source ignore unmapped field +func (q *QueryHighlight) ForceSource(b bool) *QueryHighlight { + q.params.ForceSource = &b + return q +} + +// Fragmenter sets the highlight query's fragmenter ignore unmapped field +func (q *QueryHighlight) Fragmenter(f HighlightFragmenter) *QueryHighlight { + q.params.Fragmenter = f + return q +} + +// FragmentOffset sets the highlight query's fragment_offset ignore unmapped field +func (q *QueryHighlight) FragmentOffset(i uint16) *QueryHighlight { + q.params.FragmentOffset = i + return q +} + +// HighlightQuery sets the highlight query's highlight_query ignore unmapped field +func (q *QueryHighlight) HighlightQuery(b Mappable) *QueryHighlight { + q.highlightQuery = b + return q +} + +// MatchedFields sets the highlight query's matched_fields ignore unmapped field +func (q *QueryHighlight) MatchedFields(s ...string) *QueryHighlight { + q.params.MatchedFields = append(q.params.MatchedFields,s...) + return q +} + +// NoMatchSize sets the highlight query's no_match_size ignore unmapped field +func (q *QueryHighlight) NoMatchSize(i uint16) *QueryHighlight { + q.params.NoMatchSize = i + return q +} + +// Order sets the nested highlight's score order unmapped field +func (q *QueryHighlight) Order(o HighlightOrder) *QueryHighlight { + q.params.Order = o + return q +} + +// PhraseLimit sets the highlight query's phrase_limit ignore unmapped field +func (q *QueryHighlight) PhraseLimit(i uint16) *QueryHighlight { + q.params.PhraseLimit = i + return q +} + +// RequireFieldMatch sets the highlight query's require_field_match ignore unmapped field +func (q *QueryHighlight) RequireFieldMatch(b bool) *QueryHighlight { + q.params.RequireFieldMatch = &b + return q +} + +// TagsSchema sets the highlight query's tags_schema ignore unmapped field +func (q *QueryHighlight) TagsSchema(s HighlightTagsSchema) *QueryHighlight { + q.params.TagsSchema = s + return q +} + +type HighlightType uint8 + +const ( + // HighlighterUnified is the "unified" value + HighlighterUnified HighlightType = iota + + // HighlighterPlain is the "plain" value + HighlighterPlain + + // HighlighterFvh is the "fvh" value + HighlighterFvh +) + +// String returns a string representation of the type parameter, as +// known to ElasticSearch. +func (a HighlightType) String() string { + switch a { + case HighlighterUnified: + return "unified" + case HighlighterPlain: + return "plain" + case HighlighterFvh: + return "fvh" + } + return "" +} + +type HighlightBoundaryScanner uint8 + +const ( + BoundaryScannerDefault HighlightBoundaryScanner = iota + + // BoundaryScannerChars is the "chars" value + BoundaryScannerChars + + // BoundaryScannerSentence is the "sentence" value + BoundaryScannerSentence + + // BoundaryScannerWord is the "word" value + BoundaryScannerWord +) + +// String returns a string representation of the boundary_scanner parameter, as +// known to ElasticSearch. +func (a HighlightBoundaryScanner) String() string { + switch a { + case BoundaryScannerChars: + return "chars" + case BoundaryScannerSentence: + return "sentence" + case BoundaryScannerWord: + return "word" + } + return "" +} + +type HighlightEncoder uint8 + +const ( + // EncoderDefault is the "default" value + EncoderDefault HighlightEncoder = iota + + // EncoderHtml is the "html" value + EncoderHtml +) + +// String returns a string representation of the encoder parameter, as +// known to ElasticSearch. +func (a HighlightEncoder) String() string { + switch a { + case EncoderDefault: + return "default" + case EncoderHtml: + return "html" + } + return "" +} + +type HighlightFragmenter uint8 + +const ( + // FragmentSpan is the "span" value + FragmenterSpan HighlightFragmenter = iota + + // FragmenterSimple is the "simple" value + FragmenterSimple +) + +// String returns a string representation of the fragmenter parameter, as +// known to ElasticSearch. +func (a HighlightFragmenter) String() string { + switch a { + case FragmenterSpan: + return "span" + case FragmenterSimple: + return "simple" + } + return "" +} + +type HighlightOrder uint8 + +const ( + // OrderNone is the "none" value + OrderNone HighlightOrder = iota + + // OrderScore is the "score" value + OrderScore +) + +// String returns a string representation of the order parameter, as +// known to ElasticSearch. +func (a HighlightOrder) String() string { + switch a { + case OrderNone: + return "none" + case OrderScore: + return "score" + } + return "" +} + +type HighlightTagsSchema uint8 + +const ( + TagsSchemaDefault HighlightTagsSchema = iota + // TagsSchemaStyled is the "styled" value + TagsSchemaStyled +) + +// String returns a string representation of the tags_schema parameter, as +// known to ElasticSearch. +func (a HighlightTagsSchema) String() string { + switch a { + case TagsSchemaStyled: + return "styled" + } + return "" +} diff --git a/highlight_test.go b/highlight_test.go new file mode 100644 index 0000000..76456bf --- /dev/null +++ b/highlight_test.go @@ -0,0 +1,93 @@ +package esquery + +import ( + "testing" +) + +func TestHighlight(t *testing.T) { + runMapTests(t, []mapTest{ + { + "simple highlight", + Highlight().Field("content"), + map[string]interface{}{ + "fields": map[string]interface{} { + "content": map[string]interface{}{}, + }, + }, + }, + { + "highlight all params", + Highlight(). + PreTags("
","").
+			PostTags("","
"). + Field("content", + Highlight(). + BoundaryChars(".;,")). + FragmentSize(150). + NumberOfFragments(4). + Type(HighlighterPlain). + BoundaryChars("()[]"). + BoundaryMaxScan(32). + BoundaryScanner(BoundaryScannerChars). + BoundaryScannerLocale("en-US"). + Encoder(EncoderHtml). + ForceSource(true). + Fragmenter(FragmenterSimple). + FragmentOffset(6). + HighlightQuery( + Bool(). + Must( + Match("author"). + Query("some guy"). + Analyzer("analyzer?"). + Fuzziness("fuzz"))). + MatchedFields("title","body"). + NoMatchSize(64). + Order(OrderScore). + PhraseLimit(512). + RequireFieldMatch(false). + TagsSchema(TagsSchemaStyled), + map[string]interface{}{ + "pre_tags": []string{"
",""},
+				"post_tags": []string{"","
"}, + "fragment_size": 150, + "number_of_fragments": 4, + "type": "plain", + "boundary_chars": "()[]", + "boundary_scanner": "chars", + "boundary_max_scan": 32, + "boundary_scanner_locale": "en-US", + "encoder": "html", + "force_source": true, + "fragment_offset": 6, + "fragmenter": "simple", + "matched_fields": []string{"title","body"}, + "no_match_size": 64, + "order": "score", + "phrase_limit": 512, + "require_field_match": false, + "tags_schema": "styled", + "fields": map[string]interface{}{ + "content": map[string]interface{}{ + "boundary_chars": ".;,", + }, + }, + "query": map[string]interface{} { + "bool": map[string]interface{} { + "must": []map[string]interface{} { + { + "match": map[string]interface{} { + "author": map[string]interface{} { + "analyzer": "analyzer?", + "fuzziness": "fuzz", + "query": "some guy", + }, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/search.go b/search.go index 3da937e..39dabf6 100644 --- a/search.go +++ b/search.go @@ -15,15 +15,16 @@ import ( // Not all features of the search API are currently supported, but a request can // currently include a query, aggregations, and more. type SearchRequest struct { - query Mappable aggs []Aggregation - postFilter Mappable - from *uint64 - size *uint64 explain *bool - timeout *time.Duration - source Source + from *uint64 + highlight Mappable + postFilter Mappable + query Mappable + size *uint64 sort Sort + source Source + timeout *time.Duration } // Search creates a new SearchRequest object, to be filled via method chaining. @@ -98,6 +99,13 @@ func (req *SearchRequest) SourceExcludes(keys ...string) *SearchRequest { return req } +// Highlight sets a highlight for the request. +func (req *SearchRequest) Highlight(highlight Mappable) *SearchRequest { + req.highlight = highlight + return req +} + + // Map implements the Mappable interface. It converts the request to into a // nested map[string]interface{}, as expected by the go-elasticsearch library. func (req *SearchRequest) Map() map[string]interface{} { @@ -131,6 +139,9 @@ func (req *SearchRequest) Map() map[string]interface{} { if req.timeout != nil { m["timeout"] = fmt.Sprintf("%.0fs", req.timeout.Seconds()) } + if req.highlight != nil { + m["highlight"] = req.highlight.Map() + } source := req.source.Map() if len(source) > 0 {