From f5595ecf7aa85f07209a1c560e6bd21c78a96935 Mon Sep 17 00:00:00 2001 From: CascadingRadium Date: Fri, 2 Aug 2024 12:29:31 +0530 Subject: [PATCH 1/4] MB-19243: Auto Fuzzy support --- index/scorch/scorch.go | 2 +- search/query/fuzzy.go | 56 +++++ search/query/match.go | 64 +++++- search/query/match_phrase.go | 59 ++++- search/query/multi_phrase.go | 54 ++++- search/query/phrase.go | 54 ++++- search/query/query_test.go | 318 +++++++++++++------------- search/searcher/search_fuzzy.go | 15 ++ search/searcher/search_phrase.go | 35 ++- search/searcher/search_phrase_test.go | 6 +- 10 files changed, 471 insertions(+), 192 deletions(-) diff --git a/index/scorch/scorch.go b/index/scorch/scorch.go index 7966d844d..e0cf88081 100644 --- a/index/scorch/scorch.go +++ b/index/scorch/scorch.go @@ -49,7 +49,7 @@ type Scorch struct { unsafeBatch bool - rootLock sync.RWMutex + rootLock sync.RWMutex root *IndexSnapshot // holds 1 ref-count on the root rootPersisted []chan error // closed when root is persisted diff --git a/search/query/fuzzy.go b/search/query/fuzzy.go index f24eb0c20..2021ba1f4 100644 --- a/search/query/fuzzy.go +++ b/search/query/fuzzy.go @@ -20,6 +20,7 @@ import ( "github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/search" "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" index "github.com/blevesearch/bleve_index_api" ) @@ -29,6 +30,7 @@ type FuzzyQuery struct { Fuzziness int `json:"fuzziness"` FieldVal string `json:"field,omitempty"` BoostVal *Boost `json:"boost,omitempty"` + autoFuzzy bool } // NewFuzzyQuery creates a new Query which finds @@ -66,6 +68,10 @@ func (q *FuzzyQuery) SetFuzziness(f int) { q.Fuzziness = f } +func (q *FuzzyQuery) SetAutoFuzziness(a bool) { + q.autoFuzzy = a +} + func (q *FuzzyQuery) SetPrefix(p int) { q.Prefix = p } @@ -75,5 +81,55 @@ func (q *FuzzyQuery) Searcher(ctx context.Context, i index.IndexReader, m mappin if q.FieldVal == "" { field = m.DefaultSearchField() } + if q.autoFuzzy { + return searcher.NewAutoFuzzySearcher(ctx, i, q.Term, q.Prefix, field, q.BoostVal.Value(), options) + } return searcher.NewFuzzySearcher(ctx, i, q.Term, q.Prefix, q.Fuzziness, field, q.BoostVal.Value(), options) } + +func (q *FuzzyQuery) UnmarshalJSON(data []byte) error { + type Alias FuzzyQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { + return err + } + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } + return nil +} + +// MarshalJSON customizes the JSON marshaling for FuzzyQuery +func (f *FuzzyQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type fuzzyQuery struct { + Term string `json:"term"` + Prefix int `json:"prefix_length"` + Fuzziness interface{} `json:"fuzziness"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + } + aux := fuzzyQuery{ + Term: f.Term, + Prefix: f.Prefix, + Fuzziness: fuzzyValue, + FieldVal: f.FieldVal, + BoostVal: f.BoostVal, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/match.go b/search/query/match.go index 074d11d34..81d3a461a 100644 --- a/search/query/match.go +++ b/search/query/match.go @@ -32,6 +32,7 @@ type MatchQuery struct { Prefix int `json:"prefix_length"` Fuzziness int `json:"fuzziness"` Operator MatchQueryOperator `json:"operator,omitempty"` + autoFuzzy bool } type MatchQueryOperator int @@ -107,6 +108,10 @@ func (q *MatchQuery) SetFuzziness(f int) { q.Fuzziness = f } +func (q *MatchQuery) SetAutoFuzziness(auto bool) { + q.autoFuzzy = auto +} + func (q *MatchQuery) SetPrefix(p int) { q.Prefix = p } @@ -138,10 +143,14 @@ func (q *MatchQuery) Searcher(ctx context.Context, i index.IndexReader, m mappin if len(tokens) > 0 { tqs := make([]Query, len(tokens)) - if q.Fuzziness != 0 { + if q.Fuzziness != 0 || q.autoFuzzy { for i, token := range tokens { query := NewFuzzyQuery(string(token.Term)) - query.SetFuzziness(q.Fuzziness) + if q.autoFuzzy { + query.SetAutoFuzziness(true) + } else { + query.SetFuzziness(q.Fuzziness) + } query.SetPrefix(q.Prefix) query.SetField(field) query.SetBoost(q.BoostVal.Value()) @@ -175,3 +184,54 @@ func (q *MatchQuery) Searcher(ctx context.Context, i index.IndexReader, m mappin noneQuery := NewMatchNoneQuery() return noneQuery.Searcher(ctx, i, m, options) } + +func (q *MatchQuery) UnmarshalJSON(data []byte) error { + type Alias MatchQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { + return err + } + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } + return nil +} + +// MarshalJSON customizes the JSON marshaling for FuzzyQuery +func (f *MatchQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type match struct { + Match string `json:"match"` + FieldVal string `json:"field,omitempty"` + Analyzer string `json:"analyzer,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Prefix int `json:"prefix_length"` + Fuzziness interface{} `json:"fuzziness"` + Operator MatchQueryOperator `json:"operator,omitempty"` + } + aux := match{ + Match: f.Match, + FieldVal: f.FieldVal, + Analyzer: f.Analyzer, + BoostVal: f.BoostVal, + Prefix: f.Prefix, + Fuzziness: fuzzyValue, + Operator: f.Operator, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/match_phrase.go b/search/query/match_phrase.go index 63a16a534..4589eced0 100644 --- a/search/query/match_phrase.go +++ b/search/query/match_phrase.go @@ -21,6 +21,7 @@ import ( "github.com/blevesearch/bleve/v2/analysis" "github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/util" index "github.com/blevesearch/bleve_index_api" ) @@ -30,6 +31,7 @@ type MatchPhraseQuery struct { Analyzer string `json:"analyzer,omitempty"` BoostVal *Boost `json:"boost,omitempty"` Fuzziness int `json:"fuzziness"` + autoFuzzy bool } // NewMatchPhraseQuery creates a new Query object @@ -63,6 +65,10 @@ func (q *MatchPhraseQuery) SetFuzziness(f int) { q.Fuzziness = f } +func (q *MatchPhraseQuery) SetAutoFuzziness(auto bool) { + q.autoFuzzy = auto +} + func (q *MatchPhraseQuery) Field() string { return q.FieldVal } @@ -89,7 +95,11 @@ func (q *MatchPhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m phrase := tokenStreamToPhrase(tokens) phraseQuery := NewMultiPhraseQuery(phrase, field) phraseQuery.SetBoost(q.BoostVal.Value()) - phraseQuery.SetFuzziness(q.Fuzziness) + if q.autoFuzzy { + phraseQuery.SetAutoFuzziness(true) + } else { + phraseQuery.SetFuzziness(q.Fuzziness) + } return phraseQuery.Searcher(ctx, i, m, options) } noneQuery := NewMatchNoneQuery() @@ -118,3 +128,50 @@ func tokenStreamToPhrase(tokens analysis.TokenStream) [][]string { } return nil } + +func (q *MatchPhraseQuery) UnmarshalJSON(data []byte) error { + type Alias MatchPhraseQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { + return err + } + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } + return nil +} + +// MarshalJSON customizes the JSON marshaling for FuzzyQuery +func (f *MatchPhraseQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type matchPhrase struct { + MatchPhrase string `json:"match_phrase"` + FieldVal string `json:"field,omitempty"` + Analyzer string `json:"analyzer,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness interface{} `json:"fuzziness"` + } + aux := matchPhrase{ + MatchPhrase: f.MatchPhrase, + FieldVal: f.FieldVal, + Analyzer: f.Analyzer, + BoostVal: f.BoostVal, + Fuzziness: fuzzyValue, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/multi_phrase.go b/search/query/multi_phrase.go index d1144d908..afe701dff 100644 --- a/search/query/multi_phrase.go +++ b/search/query/multi_phrase.go @@ -30,6 +30,7 @@ type MultiPhraseQuery struct { Field string `json:"field,omitempty"` BoostVal *Boost `json:"boost,omitempty"` Fuzziness int `json:"fuzziness"` + autoFuzzy bool } // NewMultiPhraseQuery creates a new Query for finding @@ -52,6 +53,10 @@ func (q *MultiPhraseQuery) SetFuzziness(f int) { q.Fuzziness = f } +func (q *MultiPhraseQuery) SetAutoFuzziness(auto bool) { + q.autoFuzzy = auto +} + func (q *MultiPhraseQuery) SetBoost(b float64) { boost := Boost(b) q.BoostVal = &boost @@ -62,7 +67,7 @@ func (q *MultiPhraseQuery) Boost() float64 { } func (q *MultiPhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { - return searcher.NewMultiPhraseSearcher(ctx, i, q.Terms, q.Fuzziness, q.Field, q.BoostVal.Value(), options) + return searcher.NewMultiPhraseSearcher(ctx, i, q.Terms, q.Fuzziness, q.autoFuzzy, q.Field, q.BoostVal.Value(), options) } func (q *MultiPhraseQuery) Validate() error { @@ -73,15 +78,46 @@ func (q *MultiPhraseQuery) Validate() error { } func (q *MultiPhraseQuery) UnmarshalJSON(data []byte) error { - type _mphraseQuery MultiPhraseQuery - tmp := _mphraseQuery{} - err := util.UnmarshalJSON(data, &tmp) - if err != nil { + type Alias MultiPhraseQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { return err } - q.Terms = tmp.Terms - q.Field = tmp.Field - q.BoostVal = tmp.BoostVal - q.Fuzziness = tmp.Fuzziness + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } return nil } + +// MarshalJSON customizes the JSON marshaling for FuzzyQuery +func (f *MultiPhraseQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type multiPhraseQuery struct { + Terms [][]string `json:"terms"` + Field string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness interface{} `json:"fuzziness"` + } + aux := multiPhraseQuery{ + Terms: f.Terms, + Field: f.Field, + BoostVal: f.BoostVal, + Fuzziness: fuzzyValue, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/phrase.go b/search/query/phrase.go index 9092e72d0..f8a3a75d4 100644 --- a/search/query/phrase.go +++ b/search/query/phrase.go @@ -30,6 +30,7 @@ type PhraseQuery struct { Field string `json:"field,omitempty"` BoostVal *Boost `json:"boost,omitempty"` Fuzziness int `json:"fuzziness"` + autoFuzzy bool } // NewPhraseQuery creates a new Query for finding @@ -54,12 +55,16 @@ func (q *PhraseQuery) SetFuzziness(f int) { q.Fuzziness = f } +func (q *PhraseQuery) SetAutoFuzziness(auto bool) { + q.autoFuzzy = auto +} + func (q *PhraseQuery) Boost() float64 { return q.BoostVal.Value() } func (q *PhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { - return searcher.NewPhraseSearcher(ctx, i, q.Terms, q.Fuzziness, q.Field, q.BoostVal.Value(), options) + return searcher.NewPhraseSearcher(ctx, i, q.Terms, q.Fuzziness, q.autoFuzzy, q.Field, q.BoostVal.Value(), options) } func (q *PhraseQuery) Validate() error { @@ -70,15 +75,46 @@ func (q *PhraseQuery) Validate() error { } func (q *PhraseQuery) UnmarshalJSON(data []byte) error { - type _phraseQuery PhraseQuery - tmp := _phraseQuery{} - err := util.UnmarshalJSON(data, &tmp) - if err != nil { + type Alias PhraseQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { return err } - q.Terms = tmp.Terms - q.Field = tmp.Field - q.BoostVal = tmp.BoostVal - q.Fuzziness = tmp.Fuzziness + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } return nil } + +// MarshalJSON customizes the JSON marshaling for FuzzyQuery +func (f *PhraseQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type phraseQuery struct { + Terms []string `json:"terms"` + Field string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness interface{} `json:"fuzziness"` + } + aux := phraseQuery{ + Terms: f.Terms, + Field: f.Field, + BoostVal: f.BoostVal, + Fuzziness: fuzzyValue, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/query_test.go b/search/query/query_test.go index 0028c956b..e1aac0e30 100644 --- a/search/query/query_test.go +++ b/search/query/query_test.go @@ -51,14 +51,14 @@ func TestParseQuery(t *testing.T) { output Query err bool }{ - { - input: []byte(`{"term":"water","field":"desc"}`), - output: func() Query { - q := NewTermQuery("water") - q.SetField("desc") - return q - }(), - }, + // { + // input: []byte(`{"term":"water","field":"desc"}`), + // output: func() Query { + // q := NewTermQuery("water") + // q.SetField("desc") + // return q + // }(), + // }, { input: []byte(`{"match":"beer","field":"desc"}`), output: func() Query { @@ -67,157 +67,157 @@ func TestParseQuery(t *testing.T) { return q }(), }, - { - input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), - output: func() Query { - q := NewMatchQuery("beer") - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), - output: func() Query { - q := NewMatchQuery("beer") - q.SetOperator(MatchQueryOperatorAnd) - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), - output: func() Query { - operator := MatchQueryOperatorAnd - q := NewMatchQuery("beer") - q.SetOperator(operator) - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), - output: func() Query { - q := NewMatchQuery("beer") - q.SetOperator(MatchQueryOperatorOr) - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), - output: func() Query { - operator := MatchQueryOperatorOr - q := NewMatchQuery("beer") - q.SetOperator(operator) - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"match":"beer","field":"desc","operator":"does not exist"}`), - output: nil, - err: true, - }, - { - input: []byte(`{"match_phrase":"light beer","field":"desc"}`), - output: func() Query { - q := NewMatchPhraseQuery("light beer") - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"must":{"conjuncts": [{"match":"beer","field":"desc"}]},"should":{"disjuncts": [{"match":"water","field":"desc"}],"min":1.0},"must_not":{"disjuncts": [{"match":"devon","field":"desc"}]}}`), - output: func() Query { - q := NewBooleanQuery( - []Query{func() Query { - q := NewMatchQuery("beer") - q.SetField("desc") - return q - }()}, - []Query{func() Query { - q := NewMatchQuery("water") - q.SetField("desc") - return q - }()}, - []Query{func() Query { - q := NewMatchQuery("devon") - q.SetField("desc") - return q - }()}) - q.SetMinShould(1) - return q - }(), - }, - { - input: []byte(`{"terms":["watered","down"],"field":"desc"}`), - output: NewPhraseQuery([]string{"watered", "down"}, "desc"), - }, - { - input: []byte(`{"query":"+beer \"light beer\" -devon"}`), - output: NewQueryStringQuery(`+beer "light beer" -devon`), - }, - { - input: []byte(`{"min":5.1,"max":7.1,"field":"desc"}`), - output: func() Query { - q := NewNumericRangeQuery(&minNum, &maxNum) - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"min":"bob","max":"cat","field":"desc"}`), - output: func() Query { - q := NewTermRangeQuery(minTerm, maxTerm) - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"start":"` + startDateStr + `","end":"` + endDateStr + `","field":"desc"}`), - output: func() Query { - q := NewDateRangeStringQuery(startDateStr, endDateStr) - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"prefix":"budwei","field":"desc"}`), - output: func() Query { - q := NewPrefixQuery("budwei") - q.SetField("desc") - return q - }(), - }, - { - input: []byte(`{"match_all":{}}`), - output: NewMatchAllQuery(), - }, - { - input: []byte(`{"match_none":{}}`), - output: NewMatchNoneQuery(), - }, - { - input: []byte(`{"ids":["a","b","c"]}`), - output: NewDocIDQuery([]string{"a", "b", "c"}), - }, - { - input: []byte(`{"bool": true}`), - output: NewBoolFieldQuery(true), - }, - { - input: []byte(`{"field": "x", "cidr": "1.2.3.0/4"}`), - output: func() Query { - q := NewIPRangeQuery("1.2.3.0/4") - q.SetField("x") - return q - }(), - }, - { - input: []byte(`{"madeitup":"queryhere"}`), - output: nil, - err: true, - }, + // { + // input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + // output: func() Query { + // q := NewMatchQuery("beer") + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), + // output: func() Query { + // q := NewMatchQuery("beer") + // q.SetOperator(MatchQueryOperatorAnd) + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), + // output: func() Query { + // operator := MatchQueryOperatorAnd + // q := NewMatchQuery("beer") + // q.SetOperator(operator) + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + // output: func() Query { + // q := NewMatchQuery("beer") + // q.SetOperator(MatchQueryOperatorOr) + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + // output: func() Query { + // operator := MatchQueryOperatorOr + // q := NewMatchQuery("beer") + // q.SetOperator(operator) + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"match":"beer","field":"desc","operator":"does not exist"}`), + // output: nil, + // err: true, + // }, + // { + // input: []byte(`{"match_phrase":"light beer","field":"desc"}`), + // output: func() Query { + // q := NewMatchPhraseQuery("light beer") + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"must":{"conjuncts": [{"match":"beer","field":"desc"}]},"should":{"disjuncts": [{"match":"water","field":"desc"}],"min":1.0},"must_not":{"disjuncts": [{"match":"devon","field":"desc"}]}}`), + // output: func() Query { + // q := NewBooleanQuery( + // []Query{func() Query { + // q := NewMatchQuery("beer") + // q.SetField("desc") + // return q + // }()}, + // []Query{func() Query { + // q := NewMatchQuery("water") + // q.SetField("desc") + // return q + // }()}, + // []Query{func() Query { + // q := NewMatchQuery("devon") + // q.SetField("desc") + // return q + // }()}) + // q.SetMinShould(1) + // return q + // }(), + // }, + // { + // input: []byte(`{"terms":["watered","down"],"field":"desc"}`), + // output: NewPhraseQuery([]string{"watered", "down"}, "desc"), + // }, + // { + // input: []byte(`{"query":"+beer \"light beer\" -devon"}`), + // output: NewQueryStringQuery(`+beer "light beer" -devon`), + // }, + // { + // input: []byte(`{"min":5.1,"max":7.1,"field":"desc"}`), + // output: func() Query { + // q := NewNumericRangeQuery(&minNum, &maxNum) + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"min":"bob","max":"cat","field":"desc"}`), + // output: func() Query { + // q := NewTermRangeQuery(minTerm, maxTerm) + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"start":"` + startDateStr + `","end":"` + endDateStr + `","field":"desc"}`), + // output: func() Query { + // q := NewDateRangeStringQuery(startDateStr, endDateStr) + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"prefix":"budwei","field":"desc"}`), + // output: func() Query { + // q := NewPrefixQuery("budwei") + // q.SetField("desc") + // return q + // }(), + // }, + // { + // input: []byte(`{"match_all":{}}`), + // output: NewMatchAllQuery(), + // }, + // { + // input: []byte(`{"match_none":{}}`), + // output: NewMatchNoneQuery(), + // }, + // { + // input: []byte(`{"ids":["a","b","c"]}`), + // output: NewDocIDQuery([]string{"a", "b", "c"}), + // }, + // { + // input: []byte(`{"bool": true}`), + // output: NewBoolFieldQuery(true), + // }, + // { + // input: []byte(`{"field": "x", "cidr": "1.2.3.0/4"}`), + // output: func() Query { + // q := NewIPRangeQuery("1.2.3.0/4") + // q.SetField("x") + // return q + // }(), + // }, + // { + // input: []byte(`{"madeitup":"queryhere"}`), + // output: nil, + // err: true, + // }, } for i, test := range tests { diff --git a/search/searcher/search_fuzzy.go b/search/searcher/search_fuzzy.go index 1957168bb..cd93b0989 100644 --- a/search/searcher/search_fuzzy.go +++ b/search/searcher/search_fuzzy.go @@ -71,6 +71,21 @@ func NewFuzzySearcher(ctx context.Context, indexReader index.IndexReader, term s boost, options, true) } +func getAutoFuzziness(term string) int { + termLength := len(term) + if termLength > 5 { + return 2 + } else if termLength > 2 { + return 1 + } + return 0 +} + +func NewAutoFuzzySearcher(ctx context.Context, indexReader index.IndexReader, term string, + prefix int, field string, boost float64, options search.SearcherOptions) (search.Searcher, error) { + return NewFuzzySearcher(ctx, indexReader, term, prefix, getAutoFuzziness(term), field, boost, options) +} + type fuzzyCandidates struct { candidates []string bytesRead uint64 diff --git a/search/searcher/search_phrase.go b/search/searcher/search_phrase.go index a7bdb2c81..bf24b465a 100644 --- a/search/searcher/search_phrase.go +++ b/search/searcher/search_phrase.go @@ -67,25 +67,32 @@ func (s *PhraseSearcher) Size() int { } func NewPhraseSearcher(ctx context.Context, indexReader index.IndexReader, terms []string, - fuzziness int, field string, boost float64, options search.SearcherOptions) (*PhraseSearcher, error) { + fuzziness int, autoFuzzy bool, field string, boost float64, options search.SearcherOptions) (*PhraseSearcher, error) { // turn flat terms []string into [][]string mterms := make([][]string, len(terms)) for i, term := range terms { mterms[i] = []string{term} } - return NewMultiPhraseSearcher(ctx, indexReader, mterms, fuzziness, field, boost, options) + return NewMultiPhraseSearcher(ctx, indexReader, mterms, fuzziness, autoFuzzy, field, boost, options) } func NewMultiPhraseSearcher(ctx context.Context, indexReader index.IndexReader, terms [][]string, - fuzziness int, field string, boost float64, options search.SearcherOptions) (*PhraseSearcher, error) { + fuzziness int, autoFuzzy bool, field string, boost float64, options search.SearcherOptions) (*PhraseSearcher, error) { options.IncludeTermVectors = true var termPositionSearchers []search.Searcher var err error var ts search.Searcher + // The following logic checks if fuzziness is enabled. + // Fuzziness is considered enabled if either: + // a. `fuzziness` is greater than 0, or + // b. `autoFuzzy` is set to true. + // if both conditions are true, `autoFuzzy` takes precedence. + // If enabled, a map will be created to store the matches for fuzzy terms. + fuzzinessEnabled := autoFuzzy || fuzziness > 0 var fuzzyTermMatches map[string][]string - if fuzziness > 0 { + if fuzzinessEnabled { fuzzyTermMatches = make(map[string][]string) ctx = context.WithValue(ctx, search.FuzzyMatchPhraseKey, fuzzyTermMatches) } @@ -95,9 +102,15 @@ func NewMultiPhraseSearcher(ctx context.Context, indexReader index.IndexReader, for _, termPos := range terms { if len(termPos) == 1 && termPos[0] != "" { // single term - if fuzziness > 0 { + if fuzzinessEnabled { // fuzzy - ts, err = NewFuzzySearcher(ctx, indexReader, termPos[0], 0, fuzziness, field, boost, options) + if autoFuzzy { + // auto fuzzy + ts, err = NewAutoFuzzySearcher(ctx, indexReader, termPos[0], 0, field, boost, options) + } else { + // non-auto fuzzy + ts, err = NewFuzzySearcher(ctx, indexReader, termPos[0], 0, fuzziness, field, boost, options) + } } else { // non-fuzzy ts, err = NewTermSearcher(ctx, indexReader, termPos[0], field, boost, options) @@ -117,9 +130,15 @@ func NewMultiPhraseSearcher(ctx context.Context, indexReader index.IndexReader, if term == "" { continue } - if fuzziness > 0 { + if fuzzinessEnabled { // fuzzy - ts, err = NewFuzzySearcher(ctx, indexReader, term, 0, fuzziness, field, boost, options) + if autoFuzzy { + // auto fuzzy + ts, err = NewAutoFuzzySearcher(ctx, indexReader, term, 0, field, boost, options) + } else { + // non-auto fuzzy + ts, err = NewFuzzySearcher(ctx, indexReader, term, 0, fuzziness, field, boost, options) + } } else { // non-fuzzy ts, err = NewTermSearcher(ctx, indexReader, term, field, boost, options) diff --git a/search/searcher/search_phrase_test.go b/search/searcher/search_phrase_test.go index 272007739..207b7b245 100644 --- a/search/searcher/search_phrase_test.go +++ b/search/searcher/search_phrase_test.go @@ -37,7 +37,7 @@ func TestPhraseSearch(t *testing.T) { }() soptions := search.SearcherOptions{Explain: true, IncludeTermVectors: true} - phraseSearcher, err := NewPhraseSearcher(nil, twoDocIndexReader, []string{"angst", "beer"}, 0, "desc", 1.0, soptions) + phraseSearcher, err := NewPhraseSearcher(nil, twoDocIndexReader, []string{"angst", "beer"}, 0, false, "desc", 1.0, soptions) if err != nil { t.Fatal(err) } @@ -131,7 +131,7 @@ func TestMultiPhraseSearch(t *testing.T) { if err != nil { t.Error(err) } - searcher, err := NewMultiPhraseSearcher(nil, reader, test.phrase, 0, "desc", 1.0, soptions) + searcher, err := NewMultiPhraseSearcher(nil, reader, test.phrase, 0, false, "desc", 1.0, soptions) if err != nil { t.Error(err) } @@ -207,7 +207,7 @@ func TestFuzzyMultiPhraseSearch(t *testing.T) { if err != nil { t.Error(err) } - searcher, err := NewMultiPhraseSearcher(context.TODO(), reader, test.mphrase, test.fuzziness, "desc", 1.0, soptions) + searcher, err := NewMultiPhraseSearcher(context.TODO(), reader, test.mphrase, test.fuzziness, false, "desc", 1.0, soptions) if err != nil { t.Error(err) } From 1565074ecd30f29de19ee4755abe80f9f8b2d344 Mon Sep 17 00:00:00 2001 From: CascadingRadium Date: Fri, 2 Aug 2024 12:51:50 +0530 Subject: [PATCH 2/4] fix --- search/query/query_test.go | 318 ++++++++++++++++++------------------- 1 file changed, 159 insertions(+), 159 deletions(-) diff --git a/search/query/query_test.go b/search/query/query_test.go index e1aac0e30..0028c956b 100644 --- a/search/query/query_test.go +++ b/search/query/query_test.go @@ -51,14 +51,14 @@ func TestParseQuery(t *testing.T) { output Query err bool }{ - // { - // input: []byte(`{"term":"water","field":"desc"}`), - // output: func() Query { - // q := NewTermQuery("water") - // q.SetField("desc") - // return q - // }(), - // }, + { + input: []byte(`{"term":"water","field":"desc"}`), + output: func() Query { + q := NewTermQuery("water") + q.SetField("desc") + return q + }(), + }, { input: []byte(`{"match":"beer","field":"desc"}`), output: func() Query { @@ -67,157 +67,157 @@ func TestParseQuery(t *testing.T) { return q }(), }, - // { - // input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), - // output: func() Query { - // q := NewMatchQuery("beer") - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), - // output: func() Query { - // q := NewMatchQuery("beer") - // q.SetOperator(MatchQueryOperatorAnd) - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), - // output: func() Query { - // operator := MatchQueryOperatorAnd - // q := NewMatchQuery("beer") - // q.SetOperator(operator) - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), - // output: func() Query { - // q := NewMatchQuery("beer") - // q.SetOperator(MatchQueryOperatorOr) - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), - // output: func() Query { - // operator := MatchQueryOperatorOr - // q := NewMatchQuery("beer") - // q.SetOperator(operator) - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"match":"beer","field":"desc","operator":"does not exist"}`), - // output: nil, - // err: true, - // }, - // { - // input: []byte(`{"match_phrase":"light beer","field":"desc"}`), - // output: func() Query { - // q := NewMatchPhraseQuery("light beer") - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"must":{"conjuncts": [{"match":"beer","field":"desc"}]},"should":{"disjuncts": [{"match":"water","field":"desc"}],"min":1.0},"must_not":{"disjuncts": [{"match":"devon","field":"desc"}]}}`), - // output: func() Query { - // q := NewBooleanQuery( - // []Query{func() Query { - // q := NewMatchQuery("beer") - // q.SetField("desc") - // return q - // }()}, - // []Query{func() Query { - // q := NewMatchQuery("water") - // q.SetField("desc") - // return q - // }()}, - // []Query{func() Query { - // q := NewMatchQuery("devon") - // q.SetField("desc") - // return q - // }()}) - // q.SetMinShould(1) - // return q - // }(), - // }, - // { - // input: []byte(`{"terms":["watered","down"],"field":"desc"}`), - // output: NewPhraseQuery([]string{"watered", "down"}, "desc"), - // }, - // { - // input: []byte(`{"query":"+beer \"light beer\" -devon"}`), - // output: NewQueryStringQuery(`+beer "light beer" -devon`), - // }, - // { - // input: []byte(`{"min":5.1,"max":7.1,"field":"desc"}`), - // output: func() Query { - // q := NewNumericRangeQuery(&minNum, &maxNum) - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"min":"bob","max":"cat","field":"desc"}`), - // output: func() Query { - // q := NewTermRangeQuery(minTerm, maxTerm) - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"start":"` + startDateStr + `","end":"` + endDateStr + `","field":"desc"}`), - // output: func() Query { - // q := NewDateRangeStringQuery(startDateStr, endDateStr) - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"prefix":"budwei","field":"desc"}`), - // output: func() Query { - // q := NewPrefixQuery("budwei") - // q.SetField("desc") - // return q - // }(), - // }, - // { - // input: []byte(`{"match_all":{}}`), - // output: NewMatchAllQuery(), - // }, - // { - // input: []byte(`{"match_none":{}}`), - // output: NewMatchNoneQuery(), - // }, - // { - // input: []byte(`{"ids":["a","b","c"]}`), - // output: NewDocIDQuery([]string{"a", "b", "c"}), - // }, - // { - // input: []byte(`{"bool": true}`), - // output: NewBoolFieldQuery(true), - // }, - // { - // input: []byte(`{"field": "x", "cidr": "1.2.3.0/4"}`), - // output: func() Query { - // q := NewIPRangeQuery("1.2.3.0/4") - // q.SetField("x") - // return q - // }(), - // }, - // { - // input: []byte(`{"madeitup":"queryhere"}`), - // output: nil, - // err: true, - // }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + output: func() Query { + q := NewMatchQuery("beer") + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), + output: func() Query { + q := NewMatchQuery("beer") + q.SetOperator(MatchQueryOperatorAnd) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), + output: func() Query { + operator := MatchQueryOperatorAnd + q := NewMatchQuery("beer") + q.SetOperator(operator) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + output: func() Query { + q := NewMatchQuery("beer") + q.SetOperator(MatchQueryOperatorOr) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + output: func() Query { + operator := MatchQueryOperatorOr + q := NewMatchQuery("beer") + q.SetOperator(operator) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"does not exist"}`), + output: nil, + err: true, + }, + { + input: []byte(`{"match_phrase":"light beer","field":"desc"}`), + output: func() Query { + q := NewMatchPhraseQuery("light beer") + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"must":{"conjuncts": [{"match":"beer","field":"desc"}]},"should":{"disjuncts": [{"match":"water","field":"desc"}],"min":1.0},"must_not":{"disjuncts": [{"match":"devon","field":"desc"}]}}`), + output: func() Query { + q := NewBooleanQuery( + []Query{func() Query { + q := NewMatchQuery("beer") + q.SetField("desc") + return q + }()}, + []Query{func() Query { + q := NewMatchQuery("water") + q.SetField("desc") + return q + }()}, + []Query{func() Query { + q := NewMatchQuery("devon") + q.SetField("desc") + return q + }()}) + q.SetMinShould(1) + return q + }(), + }, + { + input: []byte(`{"terms":["watered","down"],"field":"desc"}`), + output: NewPhraseQuery([]string{"watered", "down"}, "desc"), + }, + { + input: []byte(`{"query":"+beer \"light beer\" -devon"}`), + output: NewQueryStringQuery(`+beer "light beer" -devon`), + }, + { + input: []byte(`{"min":5.1,"max":7.1,"field":"desc"}`), + output: func() Query { + q := NewNumericRangeQuery(&minNum, &maxNum) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"min":"bob","max":"cat","field":"desc"}`), + output: func() Query { + q := NewTermRangeQuery(minTerm, maxTerm) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"start":"` + startDateStr + `","end":"` + endDateStr + `","field":"desc"}`), + output: func() Query { + q := NewDateRangeStringQuery(startDateStr, endDateStr) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"prefix":"budwei","field":"desc"}`), + output: func() Query { + q := NewPrefixQuery("budwei") + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match_all":{}}`), + output: NewMatchAllQuery(), + }, + { + input: []byte(`{"match_none":{}}`), + output: NewMatchNoneQuery(), + }, + { + input: []byte(`{"ids":["a","b","c"]}`), + output: NewDocIDQuery([]string{"a", "b", "c"}), + }, + { + input: []byte(`{"bool": true}`), + output: NewBoolFieldQuery(true), + }, + { + input: []byte(`{"field": "x", "cidr": "1.2.3.0/4"}`), + output: func() Query { + q := NewIPRangeQuery("1.2.3.0/4") + q.SetField("x") + return q + }(), + }, + { + input: []byte(`{"madeitup":"queryhere"}`), + output: nil, + err: true, + }, } for i, test := range tests { From 74a4f6ac8b86e0e70fabe130ee92fe8728f3eb1e Mon Sep 17 00:00:00 2001 From: CascadingRadium Date: Fri, 2 Aug 2024 15:44:48 +0530 Subject: [PATCH 3/4] bugfix + unit test --- search/searcher/search_fuzzy.go | 13 +++ search_test.go | 182 ++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/search/searcher/search_fuzzy.go b/search/searcher/search_fuzzy.go index cd93b0989..0f2671bfb 100644 --- a/search/searcher/search_fuzzy.go +++ b/search/searcher/search_fuzzy.go @@ -35,6 +35,19 @@ func NewFuzzySearcher(ctx context.Context, indexReader index.IndexReader, term s if fuzziness < 0 { return nil, fmt.Errorf("invalid fuzziness, negative") } + if fuzziness == 0 { + // no fuzziness, just do a term search + // check if the call is made from a phrase searcher + // and if so, add the term to the fuzzy term matches + // since the fuzzy candidate terms are not collected + // for a term search, and the only candidate term is + // the term itself + fuzzyTermMatches := ctx.Value(search.FuzzyMatchPhraseKey) + if fuzzyTermMatches != nil { + fuzzyTermMatches.(map[string][]string)[term] = []string{term} + } + return NewTermSearcher(ctx, indexReader, term, field, boost, options) + } // Note: we don't byte slice the term for a prefix because of runes. prefixTerm := "" diff --git a/search_test.go b/search_test.go index 3d14c9254..68cbd6ed6 100644 --- a/search_test.go +++ b/search_test.go @@ -3562,5 +3562,187 @@ func TestScoreBreakdown(t *testing.T) { } } } +} + +func TestAutoFuzzy(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + imap := mapping.NewIndexMapping() + + if err := imap.AddCustomAnalyzer("splitter", map[string]interface{}{ + "type": custom.Name, + "tokenizer": whitespace.Name, + "token_filters": []interface{}{lowercase.Name}, + }); err != nil { + t.Fatal(err) + } + textField := mapping.NewTextFieldMapping() + textField.Analyzer = "splitter" + textField.Store = true + textField.IncludeTermVectors = true + textField.IncludeInAll = true + + imap.DefaultMapping.Dynamic = false + imap.DefaultMapping.AddFieldMappingsAt("model", textField) + + documents := map[string]map[string]interface{}{ + "product1": { + "model": "apple iphone 12", + }, + "product2": { + "model": "apple iphone 13", + }, + "product3": { + "model": "samsung galaxy s22", + }, + "product4": { + "model": "samsung galaxy note", + }, + "product5": { + "model": "google pixel 5", + }, + "product6": { + "model": "oneplus 9 pro", + }, + "product7": { + "model": "xiaomi mi 11", + }, + "product8": { + "model": "oppo find x3", + }, + "product9": { + "model": "vivo x60 pro", + }, + "product10": { + "model": "oneplus 8t pro", + }, + "product11": { + "model": "nokia xr20", + }, + "product12": { + "model": "poco f1", + }, + "product13": { + "model": "asus rog 5", + }, + "product14": { + "model": "samsung galaxy a15 5g", + }, + "product15": { + "model": "tecno camon 17", + }, + } + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch := idx.NewBatch() + for docID, doc := range documents { + err := batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + type testStruct struct { + query string + expectHits []string + } + testQueries := []testStruct{ + { + // match query with fuzziness set to 2 + query: `{ + "match" : "applle iphone 12", + "fuzziness": 2, + "field" : "model" + }`, + expectHits: []string{"product1", "product2", "product7", "product14", "product12", "product10", "product15", "product3", "product6", "product8"}, + }, + { + // match query with fuzziness set to "auto" + query: `{ + "match" : "applle iphone 12", + "fuzziness": "auto", + "field" : "model" + }`, + expectHits: []string{"product1", "product2"}, + }, + { + // match query with fuzziness set to 2 with `and` operator + query: `{ + "match" : "applle iphone 12", + "fuzziness": 2, + "field" : "model", + "operator": "and" + }`, + expectHits: []string{"product1", "product2"}, + }, + { + // match query with fuzziness set to "auto" with `and`` operator + query: `{ + "match" : "applle iphone 12", + "fuzziness": "auto", + "field" : "model", + "operator": "and" + }`, + expectHits: []string{"product1"}, + }, + // match phrase query with fuzziness set to 2 + { + query: `{ + "match_phrase" : "onplus 9 pro", + "fuzziness": 2, + "field" : "model" + }`, + expectHits: []string{"product10", "product6"}, + }, + // match phrase query with fuzziness set to "auto" + { + query: `{ + "match_phrase" : "onplus 9 pro", + "fuzziness": "auto", + "field" : "model" + }`, + expectHits: []string{"product6"}, + }, + } + + for _, dtq := range testQueries { + q, err := query.ParseQuery([]byte(dtq.query)) + if err != nil { + t.Fatal(err) + } + + sr := NewSearchRequest(q) + sr.Highlight = NewHighlightWithStyle(ansi.Name) + sr.SortBy([]string{"-_score", "_id"}) + sr.Fields = []string{"*"} + sr.Explain = true + + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if len(res.Hits) != len(dtq.expectHits) { + t.Fatalf("expected %d hits, got %d", len(dtq.expectHits), len(res.Hits)) + } + for i, hit := range res.Hits { + if hit.ID != dtq.expectHits[i] { + t.Fatalf("expected docID %s, got %s", dtq.expectHits[i], hit.ID) + } + } + } } From 4b73bebb2b91b6d80f210b9ed4cec28ec5fd12e4 Mon Sep 17 00:00:00 2001 From: CascadingRadium Date: Fri, 2 Aug 2024 16:20:51 +0530 Subject: [PATCH 4/4] remove comment --- search/query/fuzzy.go | 1 - search/query/match.go | 1 - search/query/match_phrase.go | 1 - search/query/multi_phrase.go | 1 - search/query/phrase.go | 1 - 5 files changed, 5 deletions(-) diff --git a/search/query/fuzzy.go b/search/query/fuzzy.go index 2021ba1f4..72d7c0ea6 100644 --- a/search/query/fuzzy.go +++ b/search/query/fuzzy.go @@ -109,7 +109,6 @@ func (q *FuzzyQuery) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON customizes the JSON marshaling for FuzzyQuery func (f *FuzzyQuery) MarshalJSON() ([]byte, error) { var fuzzyValue interface{} if f.autoFuzzy { diff --git a/search/query/match.go b/search/query/match.go index 81d3a461a..ba84d9243 100644 --- a/search/query/match.go +++ b/search/query/match.go @@ -207,7 +207,6 @@ func (q *MatchQuery) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON customizes the JSON marshaling for FuzzyQuery func (f *MatchQuery) MarshalJSON() ([]byte, error) { var fuzzyValue interface{} if f.autoFuzzy { diff --git a/search/query/match_phrase.go b/search/query/match_phrase.go index 4589eced0..12a839657 100644 --- a/search/query/match_phrase.go +++ b/search/query/match_phrase.go @@ -151,7 +151,6 @@ func (q *MatchPhraseQuery) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON customizes the JSON marshaling for FuzzyQuery func (f *MatchPhraseQuery) MarshalJSON() ([]byte, error) { var fuzzyValue interface{} if f.autoFuzzy { diff --git a/search/query/multi_phrase.go b/search/query/multi_phrase.go index afe701dff..63a4d51e7 100644 --- a/search/query/multi_phrase.go +++ b/search/query/multi_phrase.go @@ -99,7 +99,6 @@ func (q *MultiPhraseQuery) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON customizes the JSON marshaling for FuzzyQuery func (f *MultiPhraseQuery) MarshalJSON() ([]byte, error) { var fuzzyValue interface{} if f.autoFuzzy { diff --git a/search/query/phrase.go b/search/query/phrase.go index f8a3a75d4..1394e8d2e 100644 --- a/search/query/phrase.go +++ b/search/query/phrase.go @@ -96,7 +96,6 @@ func (q *PhraseQuery) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON customizes the JSON marshaling for FuzzyQuery func (f *PhraseQuery) MarshalJSON() ([]byte, error) { var fuzzyValue interface{} if f.autoFuzzy {