|
- // Copyright (c) 2014 Couchbase, Inc.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
-
- package bleve
-
- import (
- "encoding/json"
- "fmt"
- "reflect"
- "time"
-
- "github.com/blevesearch/bleve/analysis"
- "github.com/blevesearch/bleve/analysis/datetime/optional"
- "github.com/blevesearch/bleve/document"
- "github.com/blevesearch/bleve/registry"
- "github.com/blevesearch/bleve/search"
- "github.com/blevesearch/bleve/search/collector"
- "github.com/blevesearch/bleve/search/query"
- "github.com/blevesearch/bleve/size"
- )
-
- var reflectStaticSizeSearchResult int
- var reflectStaticSizeSearchStatus int
-
- func init() {
- var sr SearchResult
- reflectStaticSizeSearchResult = int(reflect.TypeOf(sr).Size())
- var ss SearchStatus
- reflectStaticSizeSearchStatus = int(reflect.TypeOf(ss).Size())
- }
-
- var cache = registry.NewCache()
-
- const defaultDateTimeParser = optional.Name
-
- type numericRange struct {
- Name string `json:"name,omitempty"`
- Min *float64 `json:"min,omitempty"`
- Max *float64 `json:"max,omitempty"`
- }
-
- type dateTimeRange struct {
- Name string `json:"name,omitempty"`
- Start time.Time `json:"start,omitempty"`
- End time.Time `json:"end,omitempty"`
- startString *string
- endString *string
- }
-
- func (dr *dateTimeRange) ParseDates(dateTimeParser analysis.DateTimeParser) (start, end time.Time) {
- start = dr.Start
- if dr.Start.IsZero() && dr.startString != nil {
- s, err := dateTimeParser.ParseDateTime(*dr.startString)
- if err == nil {
- start = s
- }
- }
- end = dr.End
- if dr.End.IsZero() && dr.endString != nil {
- e, err := dateTimeParser.ParseDateTime(*dr.endString)
- if err == nil {
- end = e
- }
- }
- return start, end
- }
-
- func (dr *dateTimeRange) UnmarshalJSON(input []byte) error {
- var temp struct {
- Name string `json:"name,omitempty"`
- Start *string `json:"start,omitempty"`
- End *string `json:"end,omitempty"`
- }
-
- err := json.Unmarshal(input, &temp)
- if err != nil {
- return err
- }
-
- dr.Name = temp.Name
- if temp.Start != nil {
- dr.startString = temp.Start
- }
- if temp.End != nil {
- dr.endString = temp.End
- }
-
- return nil
- }
-
- func (dr *dateTimeRange) MarshalJSON() ([]byte, error) {
- rv := map[string]interface{}{
- "name": dr.Name,
- "start": dr.Start,
- "end": dr.End,
- }
- if dr.Start.IsZero() && dr.startString != nil {
- rv["start"] = dr.startString
- }
- if dr.End.IsZero() && dr.endString != nil {
- rv["end"] = dr.endString
- }
- return json.Marshal(rv)
- }
-
- // A FacetRequest describes a facet or aggregation
- // of the result document set you would like to be
- // built.
- type FacetRequest struct {
- Size int `json:"size"`
- Field string `json:"field"`
- NumericRanges []*numericRange `json:"numeric_ranges,omitempty"`
- DateTimeRanges []*dateTimeRange `json:"date_ranges,omitempty"`
- }
-
- func (fr *FacetRequest) Validate() error {
- nrCount := len(fr.NumericRanges)
- drCount := len(fr.DateTimeRanges)
- if nrCount > 0 && drCount > 0 {
- return fmt.Errorf("facet can only conain numeric ranges or date ranges, not both")
- }
-
- if nrCount > 0 {
- nrNames := map[string]interface{}{}
- for _, nr := range fr.NumericRanges {
- if _, ok := nrNames[nr.Name]; ok {
- return fmt.Errorf("numeric ranges contains duplicate name '%s'", nr.Name)
- }
- nrNames[nr.Name] = struct{}{}
- if nr.Min == nil && nr.Max == nil {
- return fmt.Errorf("numeric range query must specify either min, max or both for range name '%s'", nr.Name)
- }
- }
-
- } else {
- dateTimeParser, err := cache.DateTimeParserNamed(defaultDateTimeParser)
- if err != nil {
- return err
- }
- drNames := map[string]interface{}{}
- for _, dr := range fr.DateTimeRanges {
- if _, ok := drNames[dr.Name]; ok {
- return fmt.Errorf("date ranges contains duplicate name '%s'", dr.Name)
- }
- drNames[dr.Name] = struct{}{}
- start, end := dr.ParseDates(dateTimeParser)
- if start.IsZero() && end.IsZero() {
- return fmt.Errorf("date range query must specify either start, end or both for range name '%s'", dr.Name)
- }
- }
- }
- return nil
- }
-
- // NewFacetRequest creates a facet on the specified
- // field that limits the number of entries to the
- // specified size.
- func NewFacetRequest(field string, size int) *FacetRequest {
- return &FacetRequest{
- Field: field,
- Size: size,
- }
- }
-
- // AddDateTimeRange adds a bucket to a field
- // containing date values. Documents with a
- // date value falling into this range are tabulated
- // as part of this bucket/range.
- func (fr *FacetRequest) AddDateTimeRange(name string, start, end time.Time) {
- if fr.DateTimeRanges == nil {
- fr.DateTimeRanges = make([]*dateTimeRange, 0, 1)
- }
- fr.DateTimeRanges = append(fr.DateTimeRanges, &dateTimeRange{Name: name, Start: start, End: end})
- }
-
- // AddDateTimeRangeString adds a bucket to a field
- // containing date values.
- func (fr *FacetRequest) AddDateTimeRangeString(name string, start, end *string) {
- if fr.DateTimeRanges == nil {
- fr.DateTimeRanges = make([]*dateTimeRange, 0, 1)
- }
- fr.DateTimeRanges = append(fr.DateTimeRanges,
- &dateTimeRange{Name: name, startString: start, endString: end})
- }
-
- // AddNumericRange adds a bucket to a field
- // containing numeric values. Documents with a
- // numeric value falling into this range are
- // tabulated as part of this bucket/range.
- func (fr *FacetRequest) AddNumericRange(name string, min, max *float64) {
- if fr.NumericRanges == nil {
- fr.NumericRanges = make([]*numericRange, 0, 1)
- }
- fr.NumericRanges = append(fr.NumericRanges, &numericRange{Name: name, Min: min, Max: max})
- }
-
- // FacetsRequest groups together all the
- // FacetRequest objects for a single query.
- type FacetsRequest map[string]*FacetRequest
-
- func (fr FacetsRequest) Validate() error {
- for _, v := range fr {
- err := v.Validate()
- if err != nil {
- return err
- }
- }
- return nil
- }
-
- // HighlightRequest describes how field matches
- // should be highlighted.
- type HighlightRequest struct {
- Style *string `json:"style"`
- Fields []string `json:"fields"`
- }
-
- // NewHighlight creates a default
- // HighlightRequest.
- func NewHighlight() *HighlightRequest {
- return &HighlightRequest{}
- }
-
- // NewHighlightWithStyle creates a HighlightRequest
- // with an alternate style.
- func NewHighlightWithStyle(style string) *HighlightRequest {
- return &HighlightRequest{
- Style: &style,
- }
- }
-
- func (h *HighlightRequest) AddField(field string) {
- if h.Fields == nil {
- h.Fields = make([]string, 0, 1)
- }
- h.Fields = append(h.Fields, field)
- }
-
- // A SearchRequest describes all the parameters
- // needed to search the index.
- // Query is required.
- // Size/From describe how much and which part of the
- // result set to return.
- // Highlight describes optional search result
- // highlighting.
- // Fields describes a list of field values which
- // should be retrieved for result documents, provided they
- // were stored while indexing.
- // Facets describe the set of facets to be computed.
- // Explain triggers inclusion of additional search
- // result score explanations.
- // Sort describes the desired order for the results to be returned.
- // Score controls the kind of scoring performed
- //
- // A special field named "*" can be used to return all fields.
- type SearchRequest struct {
- Query query.Query `json:"query"`
- Size int `json:"size"`
- From int `json:"from"`
- Highlight *HighlightRequest `json:"highlight"`
- Fields []string `json:"fields"`
- Facets FacetsRequest `json:"facets"`
- Explain bool `json:"explain"`
- Sort search.SortOrder `json:"sort"`
- IncludeLocations bool `json:"includeLocations"`
- Score string `json:"score,omitempty"`
- }
-
- func (r *SearchRequest) Validate() error {
- if srq, ok := r.Query.(query.ValidatableQuery); ok {
- err := srq.Validate()
- if err != nil {
- return err
- }
- }
-
- return r.Facets.Validate()
- }
-
- // AddFacet adds a FacetRequest to this SearchRequest
- func (r *SearchRequest) AddFacet(facetName string, f *FacetRequest) {
- if r.Facets == nil {
- r.Facets = make(FacetsRequest, 1)
- }
- r.Facets[facetName] = f
- }
-
- // SortBy changes the request to use the requested sort order
- // this form uses the simplified syntax with an array of strings
- // each string can either be a field name
- // or the magic value _id and _score which refer to the doc id and search score
- // any of these values can optionally be prefixed with - to reverse the order
- func (r *SearchRequest) SortBy(order []string) {
- so := search.ParseSortOrderStrings(order)
- r.Sort = so
- }
-
- // SortByCustom changes the request to use the requested sort order
- func (r *SearchRequest) SortByCustom(order search.SortOrder) {
- r.Sort = order
- }
-
- // UnmarshalJSON deserializes a JSON representation of
- // a SearchRequest
- func (r *SearchRequest) UnmarshalJSON(input []byte) error {
- var temp struct {
- Q json.RawMessage `json:"query"`
- Size *int `json:"size"`
- From int `json:"from"`
- Highlight *HighlightRequest `json:"highlight"`
- Fields []string `json:"fields"`
- Facets FacetsRequest `json:"facets"`
- Explain bool `json:"explain"`
- Sort []json.RawMessage `json:"sort"`
- IncludeLocations bool `json:"includeLocations"`
- Score string `json:"score"`
- }
-
- err := json.Unmarshal(input, &temp)
- if err != nil {
- return err
- }
-
- if temp.Size == nil {
- r.Size = 10
- } else {
- r.Size = *temp.Size
- }
- if temp.Sort == nil {
- r.Sort = search.SortOrder{&search.SortScore{Desc: true}}
- } else {
- r.Sort, err = search.ParseSortOrderJSON(temp.Sort)
- if err != nil {
- return err
- }
- }
- r.From = temp.From
- r.Explain = temp.Explain
- r.Highlight = temp.Highlight
- r.Fields = temp.Fields
- r.Facets = temp.Facets
- r.IncludeLocations = temp.IncludeLocations
- r.Score = temp.Score
- r.Query, err = query.ParseQuery(temp.Q)
- if err != nil {
- return err
- }
-
- if r.Size < 0 {
- r.Size = 10
- }
- if r.From < 0 {
- r.From = 0
- }
-
- return nil
-
- }
-
- // NewSearchRequest creates a new SearchRequest
- // for the Query, using default values for all
- // other search parameters.
- func NewSearchRequest(q query.Query) *SearchRequest {
- return NewSearchRequestOptions(q, 10, 0, false)
- }
-
- // NewSearchRequestOptions creates a new SearchRequest
- // for the Query, with the requested size, from
- // and explanation search parameters.
- // By default results are ordered by score, descending.
- func NewSearchRequestOptions(q query.Query, size, from int, explain bool) *SearchRequest {
- return &SearchRequest{
- Query: q,
- Size: size,
- From: from,
- Explain: explain,
- Sort: search.SortOrder{&search.SortScore{Desc: true}},
- }
- }
-
- // IndexErrMap tracks errors with the name of the index where it occurred
- type IndexErrMap map[string]error
-
- // MarshalJSON seralizes the error into a string for JSON consumption
- func (iem IndexErrMap) MarshalJSON() ([]byte, error) {
- tmp := make(map[string]string, len(iem))
- for k, v := range iem {
- tmp[k] = v.Error()
- }
- return json.Marshal(tmp)
- }
-
- func (iem IndexErrMap) UnmarshalJSON(data []byte) error {
- var tmp map[string]string
- err := json.Unmarshal(data, &tmp)
- if err != nil {
- return err
- }
- for k, v := range tmp {
- iem[k] = fmt.Errorf("%s", v)
- }
- return nil
- }
-
- // SearchStatus is a secion in the SearchResult reporting how many
- // underlying indexes were queried, how many were successful/failed
- // and a map of any errors that were encountered
- type SearchStatus struct {
- Total int `json:"total"`
- Failed int `json:"failed"`
- Successful int `json:"successful"`
- Errors IndexErrMap `json:"errors,omitempty"`
- }
-
- // Merge will merge together multiple SearchStatuses during a MultiSearch
- func (ss *SearchStatus) Merge(other *SearchStatus) {
- ss.Total += other.Total
- ss.Failed += other.Failed
- ss.Successful += other.Successful
- if len(other.Errors) > 0 {
- if ss.Errors == nil {
- ss.Errors = make(map[string]error)
- }
- for otherIndex, otherError := range other.Errors {
- ss.Errors[otherIndex] = otherError
- }
- }
- }
-
- // A SearchResult describes the results of executing
- // a SearchRequest.
- type SearchResult struct {
- Status *SearchStatus `json:"status"`
- Request *SearchRequest `json:"request"`
- Hits search.DocumentMatchCollection `json:"hits"`
- Total uint64 `json:"total_hits"`
- MaxScore float64 `json:"max_score"`
- Took time.Duration `json:"took"`
- Facets search.FacetResults `json:"facets"`
- }
-
- func (sr *SearchResult) Size() int {
- sizeInBytes := reflectStaticSizeSearchResult + size.SizeOfPtr +
- reflectStaticSizeSearchStatus
-
- for _, entry := range sr.Hits {
- if entry != nil {
- sizeInBytes += entry.Size()
- }
- }
-
- for k, v := range sr.Facets {
- sizeInBytes += size.SizeOfString + len(k) +
- v.Size()
- }
-
- return sizeInBytes
- }
-
- func (sr *SearchResult) String() string {
- rv := ""
- if sr.Total > 0 {
- if sr.Request.Size > 0 {
- rv = fmt.Sprintf("%d matches, showing %d through %d, took %s\n", sr.Total, sr.Request.From+1, sr.Request.From+len(sr.Hits), sr.Took)
- for i, hit := range sr.Hits {
- rv += fmt.Sprintf("%5d. %s (%f)\n", i+sr.Request.From+1, hit.ID, hit.Score)
- for fragmentField, fragments := range hit.Fragments {
- rv += fmt.Sprintf("\t%s\n", fragmentField)
- for _, fragment := range fragments {
- rv += fmt.Sprintf("\t\t%s\n", fragment)
- }
- }
- for otherFieldName, otherFieldValue := range hit.Fields {
- if _, ok := hit.Fragments[otherFieldName]; !ok {
- rv += fmt.Sprintf("\t%s\n", otherFieldName)
- rv += fmt.Sprintf("\t\t%v\n", otherFieldValue)
- }
- }
- }
- } else {
- rv = fmt.Sprintf("%d matches, took %s\n", sr.Total, sr.Took)
- }
- } else {
- rv = "No matches"
- }
- if len(sr.Facets) > 0 {
- rv += fmt.Sprintf("Facets:\n")
- for fn, f := range sr.Facets {
- rv += fmt.Sprintf("%s(%d)\n", fn, f.Total)
- for _, t := range f.Terms {
- rv += fmt.Sprintf("\t%s(%d)\n", t.Term, t.Count)
- }
- if f.Other != 0 {
- rv += fmt.Sprintf("\tOther(%d)\n", f.Other)
- }
- }
- }
- return rv
- }
-
- // Merge will merge together multiple SearchResults during a MultiSearch
- func (sr *SearchResult) Merge(other *SearchResult) {
- sr.Status.Merge(other.Status)
- sr.Hits = append(sr.Hits, other.Hits...)
- sr.Total += other.Total
- if other.MaxScore > sr.MaxScore {
- sr.MaxScore = other.MaxScore
- }
- if sr.Facets == nil && len(other.Facets) != 0 {
- sr.Facets = other.Facets
- return
- }
-
- sr.Facets.Merge(other.Facets)
- }
-
- // MemoryNeededForSearchResult is an exported helper function to determine the RAM
- // needed to accommodate the results for a given search request.
- func MemoryNeededForSearchResult(req *SearchRequest) uint64 {
- if req == nil {
- return 0
- }
-
- numDocMatches := req.Size + req.From
- if req.Size+req.From > collector.PreAllocSizeSkipCap {
- numDocMatches = collector.PreAllocSizeSkipCap
- }
-
- estimate := 0
-
- // overhead from the SearchResult structure
- var sr SearchResult
- estimate += sr.Size()
-
- var dm search.DocumentMatch
- sizeOfDocumentMatch := dm.Size()
-
- // overhead from results
- estimate += numDocMatches * sizeOfDocumentMatch
-
- // overhead from facet results
- if req.Facets != nil {
- var fr search.FacetResult
- estimate += len(req.Facets) * fr.Size()
- }
-
- // highlighting, store
- var d document.Document
- if len(req.Fields) > 0 || req.Highlight != nil {
- for i := 0; i < (req.Size + req.From); i++ {
- estimate += (req.Size + req.From) * d.Size()
- }
- }
-
- return uint64(estimate)
- }
|