diff --git a/examples_test.go b/examples_test.go index edb0774e..f33968cf 100644 --- a/examples_test.go +++ b/examples_test.go @@ -416,6 +416,19 @@ func ExampleNewConjunctionQuery() { // document id 2 } +func ExampleNewMatchQueryOperator() { + query := NewMatchQueryOperator("great one", MatchQueryOperatorAnd) + searchRequest := NewSearchRequest(query) + searchResults, err := example_index.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 2 +} + func ExampleNewDisjunctionQuery() { disjuncts := make([]Query, 2) disjuncts[0] = NewMatchQuery("great") diff --git a/query_match.go b/query_match.go index 582dbd69..79ea3bcf 100644 --- a/query_match.go +++ b/query_match.go @@ -10,6 +10,7 @@ package bleve import ( + "encoding/json" "fmt" "github.com/blevesearch/bleve/index" @@ -17,12 +18,58 @@ import ( ) type matchQuery struct { - Match string `json:"match"` - FieldVal string `json:"field,omitempty"` - Analyzer string `json:"analyzer,omitempty"` - BoostVal float64 `json:"boost,omitempty"` - PrefixVal int `json:"prefix_length"` - FuzzinessVal int `json:"fuzziness"` + Match string `json:"match"` + FieldVal string `json:"field,omitempty"` + Analyzer string `json:"analyzer,omitempty"` + BoostVal float64 `json:"boost,omitempty"` + PrefixVal int `json:"prefix_length"` + FuzzinessVal int `json:"fuzziness"` + OperatorVal MatchQueryOperator `json:"operator,omitempty"` +} + +type MatchQueryOperator int + +const ( + // Document must satisfy AT LEAST ONE of term searches. + MatchQueryOperatorOr = 0 + // Document must satisfy ALL of term searches. + MatchQueryOperatorAnd = 1 +) + +func (o MatchQueryOperator) MarshalJSON() ([]byte, error) { + switch o { + case MatchQueryOperatorOr: + return json.Marshal("or") + case MatchQueryOperatorAnd: + return json.Marshal("and") + default: + return nil, fmt.Errorf("cannot marshal match operator %d to JSON", o) + } +} + +func (o *MatchQueryOperator) UnmarshalJSON(data []byte) error { + var operatorString string + err := json.Unmarshal(data, &operatorString) + if err != nil { + return err + } + + switch operatorString { + case "or": + *o = MatchQueryOperatorOr + return nil + case "and": + *o = MatchQueryOperatorAnd + return nil + default: + return matchQueryOperatorUnmarshalError(operatorString) + } +} + +type matchQueryOperatorUnmarshalError string + +func (e matchQueryOperatorUnmarshalError) Error() string { + return fmt.Sprintf("cannot unmarshal match operator '%s' from JSON", e) } // NewMatchQuery creates a Query for matching text. @@ -33,8 +80,23 @@ type matchQuery struct { // must satisfy at least one of these term searches. func NewMatchQuery(match string) *matchQuery { return &matchQuery{ - Match: match, - BoostVal: 1.0, + Match: match, + BoostVal: 1.0, + OperatorVal: MatchQueryOperatorOr, + } +} + +// NewMatchQuery creates a Query for matching text. +// An Analyzer is chosen based on the field. +// Input text is analyzed using this analyzer. +// Token terms resulting from this analysis are +// used to perform term searches. Result documents +// must satisfy term searches according to given operator. +func NewMatchQueryOperator(match string, operator MatchQueryOperator) *matchQuery { + return &matchQuery{ + Match: match, + BoostVal: 1.0, + OperatorVal: operator, } } @@ -74,6 +136,15 @@ func (q *matchQuery) SetPrefix(p int) Query { return q } +func (q *matchQuery) Operator() MatchQueryOperator { + return q.OperatorVal +} + +func (q *matchQuery) SetOperator(operator MatchQueryOperator) Query { + q.OperatorVal = operator + return q +} + func (q *matchQuery) Searcher(i index.IndexReader, m *IndexMapping, explain bool) (search.Searcher, error) { field := q.FieldVal @@ -114,10 +185,22 @@ func (q *matchQuery) Searcher(i index.IndexReader, m *IndexMapping, explain bool } } - shouldQuery := NewDisjunctionQueryMin(tqs, 1). - SetBoost(q.BoostVal) + switch q.OperatorVal { + case MatchQueryOperatorOr: + shouldQuery := NewDisjunctionQueryMin(tqs, 1). + SetBoost(q.BoostVal) - return shouldQuery.Searcher(i, m, explain) + return shouldQuery.Searcher(i, m, explain) + + case MatchQueryOperatorAnd: + mustQuery := NewConjunctionQuery(tqs). + SetBoost(q.BoostVal) + + return mustQuery.Searcher(i, m, explain) + + default: + return nil, fmt.Errorf("unhandled operator %d", q.OperatorVal) + } } noneQuery := NewMatchNoneQuery() return noneQuery.Searcher(i, m, explain) diff --git a/query_test.go b/query_test.go index 91c27619..5cd4f3ea 100644 --- a/query_test.go +++ b/query_test.go @@ -34,6 +34,23 @@ func TestParseQuery(t *testing.T) { input: []byte(`{"match":"beer","field":"desc"}`), output: NewMatchQuery("beer").SetField("desc"), }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + output: NewMatchQuery("beer").SetField("desc"), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), + output: NewMatchQueryOperator("beer", MatchQueryOperatorAnd).SetField("desc"), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + output: NewMatchQueryOperator("beer", MatchQueryOperatorOr).SetField("desc"), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"does not exist"}`), + output: nil, + err: matchQueryOperatorUnmarshalError("does not exist"), + }, { input: []byte(`{"match_phrase":"light beer","field":"desc"}`), output: NewMatchPhraseQuery("light beer").SetField("desc"),