// parseExpr3 handles NOT expressions // // expr3 = {"NOT"} expr4 func (p *Parser) parseExpr3(ctx context) (tokType, bleve.Query, error) { tok := p.next() if tok.typ != tNOT { p.backup() // just let the lower, non-NOT expression bubble up with its prefix return p.parseExpr4(ctx) } prefix, q, err := p.parseExpr4(ctx) if err != nil { return tEOF, nil, err } // KLUDGINESS - prefixes on terms in NOT expressions: // `NOT -bob` => `bob` // `NOT +bob` => `NOT bob` if prefix != tMINUS { q = bleve.NewBooleanQuery( []bleve.Query{}, []bleve.Query{}, []bleve.Query{q}, // mustNot ) } return tEOF, q, nil }
// starting point // exprList = expr1* func (p *Parser) parseExprList(ctx context) (bleve.Query, error) { // <empty> if p.peek().typ == tEOF { return bleve.NewMatchNoneQuery(), nil } must := []bleve.Query{} mustNot := []bleve.Query{} should := []bleve.Query{} for { tok := p.peek() if tok.typ == tEOF { break } // slightly kludgy... if tok.typ == tRPAREN { break } prefix, q, err := p.parseExpr1(ctx) if err != nil { return nil, err } switch prefix { case tPLUS: must = append(must, q) case tMINUS: mustNot = append(mustNot, q) default: if p.DefaultOp == AND { must = append(must, q) } else { // OR should = append(should, q) } } } total := len(must) + len(mustNot) + len(should) if total == 0 { return bleve.NewMatchNoneQuery(), nil } if total == 1 && len(must) == 1 { return must[0], nil } if total == 1 && len(should) == 1 { return should[0], nil } return bleve.NewBooleanQuery(must, should, mustNot), nil }
// parseExpr2 handles AND expressions // // expr2 = expr3 {"AND" expr3} func (p *Parser) parseExpr2(ctx context) (tokType, bleve.Query, error) { queries := []bleve.Query{} prefixes := []tokType{} for { prefix, q, err := p.parseExpr3(ctx) if err != nil { return tEOF, nil, err } prefixes = append(prefixes, prefix) queries = append(queries, q) tok := p.next() if tok.typ != tAND { p.backup() break } } // let single, non-AND expressions bubble upward, prefix intact if len(queries) == 1 { return prefixes[0], queries[0], nil } // KLUDGINESS - prefixes on terms in AND expressions // we'll ignore "+" and treat "-" as NOT // eg: // `+alice AND -bob AND chuck` => `alice AND (NOT bob) AND chuck` for i, _ := range queries { if prefixes[i] == tMINUS { queries[i] = bleve.NewBooleanQuery( []bleve.Query{}, []bleve.Query{}, []bleve.Query{queries[i]}, // mustNot ) } } return tEOF, bleve.NewConjunctionQuery(queries), nil }
// search executes a search for rooms or buildings. func (s *Server) search(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() q := query.Get("q") typeFilter := query.Get("type") results := []*models.Index{} if idx, ok := s.idIndex[q]; ok { results = append(results, idx) } else { query := bleve.NewBooleanQuery() if len(q) > 0 { /*fuzzy_query := bleve.NewFuzzyQuery(q) fuzzy_query.FuzzinessVal = 3 queryShould = append(queryShould, fuzzy_query) queryShould = append(queryShould, bleve.NewRegexpQuery("[a-zA-Z0-9_]*"+q+"[a-zA-Z0-9_]*")) queryShould = append(queryShould, bleve.NewQueryStringQuery(q))*/ query.AddShould(bleve.NewQueryStringQuery(q)) } if typeFilter != "all" { termQuery := bleve.NewTermQuery(typeFilter) query.AddMust(termQuery) } searchRequest := bleve.NewSearchRequest(query) searchRequest.Size = 25 searchResult, err := s.index.Search(searchRequest) if err != nil { http.Error(w, err.Error(), 500) return } for _, result := range []*search.DocumentMatch(searchResult.Hits) { results = append(results, s.idIndex[result.ID]) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(results) }
// parseExpr1 handles OR expressions // // expr1 = expr2 {"OR" expr2} func (p *Parser) parseExpr1(ctx context) (tokType, query.Query, error) { queries := []query.Query{} prefixes := []tokType{} for { prefix, q, err := p.parseExpr2(ctx) if err != nil { return tEOF, nil, err } prefixes = append(prefixes, prefix) queries = append(queries, q) tok := p.next() if tok.typ != tOR { p.backup() break } } // let single, non-OR expressions bubble upward, prefix intact if len(queries) == 1 { return prefixes[0], queries[0], nil } // KLUDGINESS - prefixes on terms in OR expressions // we'll ignore "+" and treat "-" as NOT // eg: // `+alice OR -bob OR chuck` => `alice OR (NOT bob) OR chuck` for i, _ := range queries { if prefixes[i] == tMINUS { q := bleve.NewBooleanQuery() q.AddMustNot(queries[i]) queries[i] = q } } return tEOF, bleve.NewDisjunctionQuery(queries...), nil }
func TestQuerySyntaxParserValid(t *testing.T) { fivePointOh := 5.0 onePointOh := 1.0 theTruth := true theFalsehood := false jan_01_2015 := numeric_util.Int64ToFloat64(time.Date(2015, time.January, 01, 0, 0, 0, 0, time.UTC).UnixNano()) jan_02_2015 := numeric_util.Int64ToFloat64(time.Date(2015, time.January, 02, 0, 0, 0, 0, time.UTC).UnixNano()) mar_15_2015 := numeric_util.Int64ToFloat64(time.Date(2015, time.March, 15, 0, 0, 0, 0, time.UTC).UnixNano()) mar_16_2015 := numeric_util.Int64ToFloat64(time.Date(2015, time.March, 16, 0, 0, 0, 0, time.UTC).UnixNano()) tests := []struct { input string result bleve.Query mapping *bleve.IndexMapping }{ { input: "test", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("test"), }, { input: `"test phrase 1"`, mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("test phrase 1"), }, { input: "field:test", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("test").SetField("field"), }, // - is allowed inside a term, just not the start { input: "field:t-est", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("t-est").SetField("field"), }, // + is allowed inside a term, just not the start { input: "field:t+est", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("t+est").SetField("field"), }, // > is allowed inside a term, just not the start { input: "field:t>est", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("t>est").SetField("field"), }, // < is allowed inside a term, just not the start { input: "field:t<est", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("t<est").SetField("field"), }, // = is allowed inside a term, just not the start { input: "field:t=est", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("t=est").SetField("field"), }, { input: "+field1:test1", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("test1").SetField("field1"), }, { input: "-field2:test2", mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( nil, nil, []bleve.Query{ bleve.NewMatchPhraseQuery("test2").SetField("field2"), }), }, { input: `field3:"test phrase 2"`, mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("test phrase 2").SetField("field3"), }, { input: `+field4:"test phrase 1"`, mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("test phrase 1").SetField("field4"), }, { input: `-field5:"test phrase 2"`, mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( nil, nil, []bleve.Query{ bleve.NewMatchPhraseQuery("test phrase 2").SetField("field5"), }), }, { input: `+field6:test3 -field7:test4 field8:test5`, mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( []bleve.Query{ bleve.NewMatchPhraseQuery("test3").SetField("field6"), }, []bleve.Query{ bleve.NewMatchPhraseQuery("test5").SetField("field8"), }, []bleve.Query{ bleve.NewMatchPhraseQuery("test4").SetField("field7"), }), }, { input: "test^3", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("test").SetBoost(3.0), }, { input: "test^3 other^6", mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( nil, []bleve.Query{ bleve.NewMatchPhraseQuery("test").SetBoost(3.0), bleve.NewMatchPhraseQuery("other").SetBoost(6.0), }, nil, ), }, { input: "33", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("33"), }, { input: "field:33", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("33").SetField("field"), }, { input: "cat-dog", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("cat-dog"), }, /* // TODO: MatchPhraseQuery doesn't handle fuzziness... { input: "watex~", mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("watex").SetFuzziness(1), }, { input: "watex~2", mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( nil, []bleve.Query{ bleve.NewMatchQuery("watex").SetFuzziness(2), }, nil), }, { input: "watex~ 2", mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( nil, []bleve.Query{ bleve.NewMatchQuery("watex").SetFuzziness(1), bleve.NewMatchQuery("2"), }, nil), }, { input: "field:watex~", mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( nil, []bleve.Query{ bleve.NewMatchQuery("watex").SetFuzziness(1).SetField("field"), }, nil), }, { input: "field:watex~2", mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( nil, []bleve.Query{ bleve.NewMatchQuery("watex").SetFuzziness(2).SetField("field"), }, nil), }, */ { input: `field:555c3bb06f7a127cda000005`, mapping: NewIndexMapping(), result: bleve.NewMatchPhraseQuery("555c3bb06f7a127cda000005").SetField("field"), }, { input: `field:>5`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&fivePointOh, nil, &theFalsehood, nil).SetField("field"), }, { input: `field:>=5`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&fivePointOh, nil, &theTruth, nil).SetField("field"), }, { input: `field:<5`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(nil, &fivePointOh, nil, &theFalsehood).SetField("field"), }, { input: `field:<=5`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(nil, &fivePointOh, nil, &theTruth).SetField("field"), }, { input: `grapefruit AND lemon`, mapping: NewIndexMapping(), result: bleve.NewConjunctionQuery([]bleve.Query{ bleve.NewMatchPhraseQuery("grapefruit"), bleve.NewMatchPhraseQuery("lemon"), }), }, { input: `grapefruit OR lemon`, mapping: NewIndexMapping(), result: bleve.NewDisjunctionQuery([]bleve.Query{ bleve.NewMatchPhraseQuery("grapefruit"), bleve.NewMatchPhraseQuery("lemon"), }), }, { // default operator is OR input: `grapefruit lemon`, mapping: NewIndexMapping(), result: bleve.NewBooleanQuery( nil, []bleve.Query{ bleve.NewMatchPhraseQuery("grapefruit"), bleve.NewMatchPhraseQuery("lemon"), }, nil, ), }, { input: `grapefruit AND NOT lemon`, mapping: NewIndexMapping(), result: bleve.NewConjunctionQuery([]bleve.Query{ bleve.NewMatchPhraseQuery("grapefruit"), bleve.NewBooleanQuery(nil, nil, []bleve.Query{bleve.NewMatchPhraseQuery("lemon")}), }), }, { input: `field:(grapefruit AND lemon)`, mapping: NewIndexMapping(), result: bleve.NewConjunctionQuery([]bleve.Query{ bleve.NewMatchPhraseQuery("grapefruit").SetField("field"), bleve.NewMatchPhraseQuery("lemon").SetField("field"), }), }, { input: `-field:(grapefruit AND lemon)`, mapping: NewIndexMapping(), result: bleve.NewBooleanQuery(nil, nil, []bleve.Query{ bleve.NewConjunctionQuery([]bleve.Query{ bleve.NewMatchPhraseQuery("grapefruit").SetField("field"), bleve.NewMatchPhraseQuery("lemon").SetField("field"), }), }), }, { input: `shoesize:[1 TO 5]`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&onePointOh, &fivePointOh, &theTruth, &theTruth).SetField("shoesize"), }, { input: `shoesize:{1 TO 5}`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&onePointOh, &fivePointOh, &theFalsehood, &theFalsehood).SetField("shoesize"), }, { input: `shoesize:[1 TO 5}`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&onePointOh, &fivePointOh, &theTruth, &theFalsehood).SetField("shoesize"), }, { input: `shoesize:{1 TO 5]`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&onePointOh, &fivePointOh, &theFalsehood, &theTruth).SetField("shoesize"), }, { input: `shoesize:[ TO 5]`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(nil, &fivePointOh, nil, &theTruth).SetField("shoesize"), }, { input: `shoesize:[1 TO ]`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&onePointOh, nil, &theTruth, nil).SetField("shoesize"), }, // date ranges (note that endpoints and inclusivity might be modified by the parser) { input: `when:[2015-01-01 TO 2015-03-15]`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&jan_01_2015, &mar_16_2015, &theTruth, &theFalsehood).SetField("when"), }, { input: `when:{2015-01-01 TO 2015-03-15]`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&jan_02_2015, &mar_16_2015, &theTruth, &theFalsehood).SetField("when"), }, { input: `when:[2015-01-01 TO 2015-03-15}`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&jan_01_2015, &mar_15_2015, &theTruth, &theFalsehood).SetField("when"), }, { input: `when:>2015-03-15`, mapping: NewIndexMapping(), result: bleve.NewNumericRangeInclusiveQuery(&mar_16_2015, nil, &theTruth, nil).SetField("when"), }, } for _, test := range tests { q, err := Parse(test.input) if err != nil { t.Error(err) } if !reflect.DeepEqual(q, test.result) { t.Errorf("Expected %#v, got %#v: for `%s`", test.result, q, test.input) // t.Errorf("Expected %#v, got %#v: for %s", test.result.(*booleanQuery).Should.(*disjunctionQuery).Disjuncts[0], q.(*booleanQuery).Should.(*disjunctionQuery).Disjuncts[0], test.input) } } }
// starting point // exprList = expr1* func (p *Parser) parseExprList(ctx context) (query.Query, error) { // <empty> if p.peek().typ == tEOF { return bleve.NewMatchNoneQuery(), nil } must := []query.Query{} mustNot := []query.Query{} should := []query.Query{} for { tok := p.peek() if tok.typ == tEOF { break } // slightly kludgy... if tok.typ == tRPAREN { break } prefix, q, err := p.parseExpr1(ctx) if err != nil { return nil, err } switch prefix { case tPLUS: must = append(must, q) case tMINUS: mustNot = append(mustNot, q) default: if p.DefaultOp == AND { must = append(must, q) } else { // OR should = append(should, q) } } } // some obvious shortcuts total := len(must) + len(mustNot) + len(should) if total == 0 { return bleve.NewMatchNoneQuery(), nil } if total == 1 && len(must) == 1 { return must[0], nil } if total == 1 && len(should) == 1 { return should[0], nil } // no shortcuts - go with the full-fat version q := bleve.NewBooleanQuery() if len(must) > 0 { q.AddMust(must...) } if len(should) > 0 { q.AddShould(should...) } if len(mustNot) > 0 { q.AddMustNot(mustNot...) } return q, nil }
func getJobSearchRequest(r *http.Request) (*bleve.SearchRequest, error) { musts := []query.Query{} // mustNots := []query.Query{} // shoulds := []query.Query{} tagShoulds := []query.Query{} for _, tag := range r.URL.Query()["tags"] { tagShoulds = append(tagShoulds, bleve.NewMatchQuery(tag)) } if len(tagShoulds) > 0 { booleanQuery := bleve.NewBooleanQuery() booleanQuery.AddShould(tagShoulds...) musts = append(musts, booleanQuery) } value1 := 0.0 if len(r.URL.Query().Get("price_from")) != 0 { intValue1, err := strconv.ParseInt(r.URL.Query().Get("price_from"), 10, 64) if err != nil { return nil, err } value1 = float64(intValue1) } value2 := math.MaxFloat64 if len(r.URL.Query().Get("price_to")) != 0 { intValue2, err := strconv.ParseInt(r.URL.Query().Get("price_to"), 10, 64) if err != nil { return nil, err } value2 = float64(intValue2) } inclusiveValue1 := true inclusiveValue2 := false numericRangeIncludiveQuery := bleve.NewNumericRangeInclusiveQuery( &value1, &value2, &inclusiveValue1, &inclusiveValue2, ) numericRangeIncludiveQuery.SetField("price") musts = append(musts, numericRangeIncludiveQuery) period := int64(30) if len(r.URL.Query().Get("period")) != 0 { periodTemp, err := strconv.ParseInt(r.URL.Query().Get("period"), 10, 64) if err != nil { return nil, err } if periodTemp > 0 && periodTemp <= 365 { period = periodTemp } } now := time.Now() dateTo := time.Now().Add(time.Duration(24*period) * time.Hour) dateRangeQuery := bleve.NewDateRangeQuery(now, dateTo) dateRangeQuery.SetField("startDate") musts = append(musts, dateRangeQuery) query := bleve.NewBooleanQuery() query.AddMust(musts...) // query.AddMustNot(mustNots...) // query.AddShould(shoulds...) searchRequest := bleve.NewSearchRequest(query) searchRequest.Fields = []string{"*"} return searchRequest, nil }