diff --git a/vendor/github.com/bcampbell/qs/LICENSE b/vendor/github.com/bcampbell/qs/LICENSE new file mode 100644 index 0000000..2330426 --- /dev/null +++ b/vendor/github.com/bcampbell/qs/LICENSE @@ -0,0 +1,14 @@ +Copyright 2015 Ben Campbell + + 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. + diff --git a/vendor/github.com/bcampbell/qs/README.md b/vendor/github.com/bcampbell/qs/README.md new file mode 100644 index 0000000..28a521e --- /dev/null +++ b/vendor/github.com/bcampbell/qs/README.md @@ -0,0 +1,77 @@ +[![GoDoc](https://godoc.org/github.com/bcampbell/qs?status.svg)](https://godoc.org/github.com/bcampbell/qs) + +## Overview + +qs implements a query language for the [Bleve](http://www.blevesearch.com/) +text indexer library. + +The query syntax is aiming to be about the same as those used by +[Lucene](http://lucene.apache.org/core/5_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description) +and [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax). + +The full syntax is described in [syntax.md](syntax.md). + +## Usage + + import "github.com/bcampbell/qs" + + query, err := qs.Parse("grapefruit lemon orange lime") + +Or, to create a parse which uses AND as the default operator: + + p := qs.Parser{DefaultOp: qs.AND} + query,err := p.Parse("grapefruit lemon orange lime") + + + +## Quick Examples + +Documents containing the term `grapefruit`: + + grapefruit + +There is support for boolean AND, OR and NOT: + + grapefruit OR lemon + grapefruit AND lemon + grapefruit AND NOT lemon + +Phrases are indicated with quotes: + + "navel orange" + +The default boolean operator is OR, so these two are equivalent when using the default parser: + + grapefruit lemon + grapefruit OR lemon + + +Fields: + + tags:citrus + headline:"How to Make the Perfect Negroni" + +Grouping: + + (lemon AND lime) OR (orange AND grapefruit) + +Boosting term relevance: + + fruit grapefruit^2 + +Inclusive ranges in square brackets, Exclusive ranges in curly brackets: + + date:[2012-01-01 TO 2012-12-31] + count:{0 TO 100} + +Wildcard markers `*` (any character sequence, 0 or more) and `?` (any single character) +eg to match `foot`, `fort`, `fret` etc: + + f??t + +eg to match `ant`, `anteater`, `antidisestablishmentarianism` etc: + + ant* + + + diff --git a/vendor/github.com/bcampbell/qs/doc.go b/vendor/github.com/bcampbell/qs/doc.go new file mode 100644 index 0000000..3141f66 --- /dev/null +++ b/vendor/github.com/bcampbell/qs/doc.go @@ -0,0 +1,17 @@ +/* +Package qs is a query language parser for Bleve (http://www.blevesearch.com). + +Example: + + import "github.com/bcampbell/qs" + + query, err := qs.Parse("grapefruit lemon orange lime") + +Or, to create a parse which uses AND as the default operator: + + p := qs.Parser{DefaultOp: qs.AND} + query,err := p.Parse("grapefruit lemon orange lime") + + +*/ +package qs diff --git a/vendor/github.com/bcampbell/qs/lex.go b/vendor/github.com/bcampbell/qs/lex.go new file mode 100644 index 0000000..d23eddf --- /dev/null +++ b/vendor/github.com/bcampbell/qs/lex.go @@ -0,0 +1,251 @@ +package qs + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +// TODO: handle escaping special characters: + +type tokType int + +const ( + tEOF tokType = iota + tERROR + tLITERAL + tQUOTED + tPLUS + tMINUS + tCOLON + tEQUAL + tGREATER + tLESS + tOR + tAND + tNOT + tLPAREN + tRPAREN + tLSQUARE + tRSQUARE + tLBRACE + tRBRACE + tTO + tBOOST + tFUZZY +) + +func (t tokType) String() string { + + tokTypes := map[tokType]string{ + tEOF: "tEOF", + tERROR: "tERROR", + tLITERAL: "tLITERAL", + tQUOTED: "tQUOTED", + tOR: "tOR", + tAND: "tAND", + tNOT: "tNOT", + tTO: "tTO", + tPLUS: "tPLUS", + tMINUS: "tMINUS", + tCOLON: "tCOLON", + tEQUAL: "tEQUAL", + tGREATER: "tGREATER", + tLESS: "tLESS", + tLPAREN: "tLPAREN", + tRPAREN: "tRPAREN", + tLSQUARE: "tLSQUARE", + tRSQUARE: "tRSQUARE", + tLBRACE: "tLBRACE", + tRBRACE: "tRBRACE", + tBOOST: "tBOOST", + tFUZZY: "tFUZZY", + } + return tokTypes[t] +} + +// some single-rune tokens +var singles = map[rune]tokType{ + '(': tLPAREN, + ')': tRPAREN, + '[': tLSQUARE, + ']': tRSQUARE, + '{': tLBRACE, + '}': tRBRACE, + ':': tCOLON, + '+': tPLUS, + '-': tMINUS, + '=': tEQUAL, + '>': tGREATER, + '<': tLESS, +} + +type token struct { + typ tokType + val string + pos int +} + +type stateFn func(*lexer) stateFn + +type lexer struct { + input string + tokens []token + pos int + prevpos int + start int +} + +// lex takes an input string and breaks it up into an array of tokens. +// The last token will be an tEOF, unless an error occurs, in which case +// it will be a tERROR. +func lex(input string) []token { + l := &lexer{ + input: input, + tokens: []token{}, + } + // run state machine - each state returns the next state, or nil when finished + for state := lexDefault; state != nil; { + state = state(l) + } + return l.tokens +} + +func (l *lexer) next() rune { + l.prevpos = l.pos + if l.eof() { + return 0 + } + r, w := utf8.DecodeRuneInString(l.input[l.pos:]) + l.pos += w + return r +} +func (l *lexer) eof() bool { + return l.pos >= len(l.input) +} + +func (l *lexer) ignore() { + l.start = l.pos +} +func (l *lexer) backup() { + l.pos = l.prevpos +} +func (l *lexer) peek() rune { + r := l.next() + l.backup() + return r +} + +func (l *lexer) emit(t tokType) { + l.tokens = append(l.tokens, token{t, l.input[l.start:l.pos], l.start}) + l.start = l.pos +} + +func (l *lexer) emitError(msg string) { + l.tokens = append(l.tokens, token{tERROR, msg, l.start}) + l.start = l.pos +} + +func lexDefault(l *lexer) stateFn { + // skip space + for { + if l.eof() { + l.emit(tEOF) + return nil + } + r := l.next() + if !unicode.IsSpace(r) { + l.backup() + l.ignore() + if r == '~' || r == '^' { + return lexSuffix + } + if typ, got := singles[r]; got { + l.next() + l.emit(typ) + return lexDefault + } + + if r == '"' || r == '\'' { + return lexQuoted + } + + return lexText + } + } +} + +func lexText(l *lexer) stateFn { + // non-space characters which terminate a literal + //stopChars := `&|:(){}[]^~` + stopChars := `:(){}[]^~` + + for { + if l.eof() { + break + } + r := l.next() + if unicode.IsSpace(r) || strings.ContainsRune(stopChars, r) { + l.backup() + break + } + } + + switch l.input[l.start:l.pos] { + case "OR": + l.emit(tOR) + case "AND": + l.emit(tAND) + case "NOT": + l.emit(tNOT) + case "TO": + l.emit(tTO) + default: + l.emit(tLITERAL) + } + + return lexDefault +} + +func lexQuoted(l *lexer) stateFn { + q := l.next() + for { + if l.eof() { + l.emitError("unclosed quote") + return nil + } + r := l.next() + if r == q { + break + } + } + l.emit(tQUOTED) + return lexDefault +} + +func lexSuffix(l *lexer) stateFn { + kind := l.next() // '^' or '~' + + // number (optional) + for { + if l.eof() { + break + } + r := l.next() + if unicode.IsSpace(r) { + l.backup() + break + } + if !strings.ContainsRune("0123456789.", r) { + l.emitError("bad number") + } + } + + switch kind { + case '~': + l.emit(tFUZZY) + case '^': + l.emit(tBOOST) + } + return lexDefault +} diff --git a/vendor/github.com/bcampbell/qs/parse.go b/vendor/github.com/bcampbell/qs/parse.go new file mode 100644 index 0000000..518f05f --- /dev/null +++ b/vendor/github.com/bcampbell/qs/parse.go @@ -0,0 +1,621 @@ +package qs + +import ( + "fmt" + "github.com/blevesearch/bleve" + "github.com/blevesearch/bleve/search/query" + "strconv" + "strings" + "time" +) + +// ParseError is the error type returned by Parse() +type ParseError struct { + // Pos is the character position where the error occured + Pos int + // Msg is a description of the error + Msg string +} + +func (pe ParseError) Error() string { return fmt.Sprintf("%d: %s", pe.Pos, pe.Msg) } + +type OpType int + +const ( + OR OpType = 0 + AND = 1 +) + +type Parser struct { + tokens []token + pos int + // DefaultOp is used when no explict OR or AND is present + // ie: foo bar => foo OR bar | foo AND bar + // TODO: not sure AND/OR is the right terminology (but it's what others use) + // Doesn't actually use OR or AND. AND is treated as an implied '+' prefix + DefaultOp OpType + + // Loc is the location to use for parsing dates in range queries. + // If nil, UTC is assumed. + Loc *time.Location +} + +// context is used to hold settings active within a given scope during parsing +// (TODO: maybe this should just be absorbed back into the Parser struct instead?) +type context struct { + // field is the name of the field currently in scope (or "") + field string + fieldPos int +} + +// Parse takes a query string and turns it into a bleve Query. +// +// Returned errors are type ParseError, which includes the position +// of the offending part of the input string. +// +// BNF(ish) query syntax: +// exprList = expr1* +// expr1 = expr2 {"OR" expr2} +// expr2 = expr3 {"AND" expr3} +// expr3 = {"NOT"} expr4 +// expr4 = {("+"|"-")} expr5 +// expr5 = {field} part {boost} +// part = lit {"~" number} | range | "(" exprList ")" +// field = lit ":" +// range = ("["|"}") {lit} "TO" {lit} ("]"|"}") +// relational = ("<"|">"|"<="|">=") lit +// boost = "^" number +// +// (where lit is a string, quoted string or number) +func (p *Parser) Parse(q string) (query.Query, error) { + p.tokens = lex(q) + ctx := context{field: ""} + return p.parseExprList(ctx) +} + +// Parse takes a query string and turns it into a bleve Query using +// the default Parser. +// Returned errors are type ParseError, which includes the position +// of the offending part of the input string. +func Parse(q string) (query.Query, error) { + p := Parser{DefaultOp: OR} + return p.Parse(q) +} + +// peek looks at the next token without consuming it. +// peeks beyond the end of the token stream will return EOF +func (p *Parser) peek() token { + if p.pos < len(p.tokens) { + tok := p.tokens[p.pos] + return tok + } + return token{typ: tEOF} +} + +// backup steps back one position in the token stream +func (p *Parser) backup() { + p.pos -= 1 +} + +// next fetches the next token in the stream +func (p *Parser) next() token { + if p.pos < len(p.tokens) { + tok := p.tokens[p.pos] + p.pos += 1 + return tok + } + p.pos += 1 // to make sure backup() works + return token{typ: tEOF} +} + +// starting point +// exprList = expr1* +func (p *Parser) parseExprList(ctx context) (query.Query, error) { + // + 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 +} + +// 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 +} + +// parseExpr2 handles AND expressions +// +// expr2 = expr3 {"AND" expr3} +func (p *Parser) parseExpr2(ctx context) (tokType, query.Query, error) { + + queries := []query.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 { + q := bleve.NewBooleanQuery() + q.AddMustNot(queries[i]) + queries[i] = q + } + } + + return tEOF, bleve.NewConjunctionQuery(queries...), nil +} + +// parseExpr3 handles NOT expressions +// +// expr3 = {"NOT"} expr4 +func (p *Parser) parseExpr3(ctx context) (tokType, query.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 { + q2 := bleve.NewBooleanQuery() + q2.AddMustNot(q) + q = q2 + } + return tEOF, q, nil +} + +// Here's where all the prefix-bubbling-up begins... +// expr4 = {("+"|"-")} expr5 +func (p *Parser) parseExpr4(ctx context) (tokType, query.Query, error) { + var prefix tokType + tok := p.next() + switch tok.typ { + case tMINUS, tPLUS: + prefix = tok.typ + default: + p.backup() + prefix = tEOF + } + + q, err := p.parseExpr5(ctx) + return prefix, q, err + +} + +// expr5 = {field} part {boost} +func (p *Parser) parseExpr5(ctx context) (query.Query, error) { + + fldpos := p.peek().pos + fld, err := p.parseField() + if err != nil { + return nil, err + } + if fld != "" { + if ctx.field != "" { + return nil, ParseError{fldpos, fmt.Sprintf("'%s:' clashes with '%s:'", fld, ctx.field)} + } + ctx.field = fld + ctx.fieldPos = fldpos + } + + q, err := p.parsePart(ctx) + if err != nil { + return nil, err + } + + // parse (optional) suffix + boostpos := p.peek().pos + boost, err := p.parseBoostSuffix() + if err != nil { + return nil, err + } + if boost > 0 { + if boostable, ok := q.(query.BoostableQuery); ok { + boostable.SetBoost(boost) + } else { + return nil, ParseError{boostpos, "can't specify a boost value here"} + } + } + + return q, nil +} + +// part = lit {"~" number} | range | "(" exprList ")" +func (p *Parser) parsePart(ctx context) (query.Query, error) { + + tok := p.next() + + // lit + if tok.typ == tLITERAL { + var q query.Query + if strings.ContainsAny(tok.val, "*?") { + q = bleve.NewWildcardQuery(tok.val) + } else { + if p.peek().typ == tFUZZY { + fuzziness, err := p.parseFuzzySuffix() + if err != nil { + return nil, err + } + fuzz := bleve.NewFuzzyQuery(tok.val) + fuzz.SetFuzziness(fuzziness) + q = fuzz + } else { + q = bleve.NewMatchPhraseQuery(tok.val) + } + } + if ctx.field != "" { + if fieldable, ok := q.(query.FieldableQuery); ok { + fieldable.SetField(ctx.field) + } else { + return nil, ParseError{ctx.fieldPos, "unexpected field"} + } + } + return q, nil + } + if tok.typ == tQUOTED { + // strip quotes (ugh) + txt := string(tok.val[1 : len(tok.val)-1]) + /* + if strings.ContainsAny(txt, "*?") { + return nil, ParseError{tok.pos, "wildcards not supported in phrases"} + } + */ + q := bleve.NewMatchPhraseQuery(txt) + if ctx.field != "" { + q.SetField(ctx.field) + } + return q, nil + } + + // | "(" exprList ")" + if tok.typ == tLPAREN { + q, err := p.parseExprList(ctx) + if err != nil { + return nil, err + } + tok = p.next() + if tok.typ != tRPAREN { + return nil, ParseError{tok.pos, "missing )"} + } + return q, nil + } + + // | range + if tok.typ == tLSQUARE || tok.typ == tLBRACE { + p.backup() + q, err := p.parseRange(ctx) + if err != nil { + return nil, err + } + return q, nil + } + + // | relational + if tok.typ == tGREATER || tok.typ == tLESS { + p.backup() + q, err := p.parseRelational(ctx) + if err != nil { + return nil, err + } + return q, nil + } + + if tok.typ == tERROR { + return nil, ParseError{tok.pos, tok.val} + } + + return nil, ParseError{tok.pos, fmt.Sprintf("unexpected %s", tok.val)} +} + +// returns >0 if there is a value given +func (p *Parser) parseBoostSuffix() (float64, error) { + tok := p.next() + if tok.typ != tBOOST { + p.backup() + return 0, nil + } + + v := tok.val[1:] + if v == "" { + return 1.0, nil + } + boost, err := strconv.ParseFloat(v, 64) + if err != nil { + return 0, ParseError{tok.pos, "bad boost value"} + } + + return boost, nil +} + +// +func (p *Parser) parseFuzzySuffix() (int, error) { + tok := p.next() + if tok.typ != tFUZZY { + return 0, ParseError{tok.pos, "expected ~"} + } + + v := tok.val[1:] + if v == "" { + return 1, nil // Default fuzziness is 1 + } + fuzz, err := strconv.Atoi(v) + if err != nil { + return 0, ParseError{tok.pos, "bad fuzziness value"} + } + + return fuzz, nil +} + +// parse (optional) field specifier +// [ lit ":" ] +// returns field name or "" if not a field +func (p *Parser) parseField() (string, error) { + tok := p.next() + if tok.typ != tLITERAL { + // not a field + p.backup() + return "", nil + } + field := tok.val + + tok = p.next() + if tok.typ != tCOLON { + // oop, not a field after all! + p.backup() + p.backup() + return "", nil + } + + return field, nil // it's OK +} + +// range = ("["|"}") {lit} "TO" {lit} ("]"|"}") +func (p *Parser) parseRange(ctx context) (query.Query, error) { + + var minVal, maxVal string + var minInclusive, maxInclusive bool + openTok := p.next() + switch openTok.typ { + case tLSQUARE: + minInclusive = true + case tLBRACE: + minInclusive = false + default: + return nil, ParseError{openTok.pos, "expected range"} + } + + tok := p.next() + switch tok.typ { + case tLITERAL: + minVal = tok.val + case tQUOTED: + minVal = tok.val[1 : len(tok.val)-1] + case tTO: + p.backup() + // empty start + default: + return nil, ParseError{tok.pos, fmt.Sprintf("unexpected %s", tok.val)} + } + + tok = p.next() + if tok.typ != tTO { + return nil, ParseError{tok.pos, "expected TO"} + } + + tok = p.next() + switch tok.typ { + case tLITERAL: + maxVal = tok.val + case tQUOTED: + maxVal = tok.val[1 : len(tok.val)-1] + case tRSQUARE: + p.backup() // empty end value + case tRBRACE: + p.backup() // empty end value + default: + return nil, ParseError{tok.pos, fmt.Sprintf("unexpected %s", tok.val)} + } + + closeTok := p.next() + switch closeTok.typ { + case tRSQUARE: + maxInclusive = true + case tRBRACE: + maxInclusive = false + default: + return nil, ParseError{closeTok.pos, "expected ] or }"} + } + + rp := newRangeParams(minVal, maxVal, minInclusive, maxInclusive, p.Loc) + q, err := rp.generate() + if err != nil { + return nil, ParseError{openTok.pos, err.Error()} + } + if ctx.field != "" { + if fieldable, ok := q.(query.FieldableQuery); ok { + fieldable.SetField(ctx.field) + } else { + return nil, ParseError{ctx.fieldPos, "unexpected field"} + } + } + return q, nil +} + +// parseRelational handles greaterthan/lessthan etc... +// Implemented as a range. +// relational = ("<"|">"|"<="|">=") lit +func (p *Parser) parseRelational(ctx context) (query.Query, error) { + + var minVal, maxVal string + var minInclusive, maxInclusive bool + + rel := p.next() + if rel.typ != tGREATER && rel.typ != tLESS { + return nil, ParseError{rel.pos, "expected > or <"} + } + + eq := p.next() + if eq.typ != tEQUAL { + p.backup() + } + + var val string + tok := p.next() + switch tok.typ { + case tLITERAL: + val = tok.val + case tQUOTED: + val = tok.val[1 : len(tok.val)-1] + default: + return nil, ParseError{tok.pos, fmt.Sprintf("unexpected %s", tok.val)} + } + + if rel.typ == tGREATER { + minVal = val + minInclusive = (eq.typ == tEQUAL) + } else { // if rel.typ == tLESS + maxVal = val + maxInclusive = (eq.typ == tEQUAL) + } + + rp := newRangeParams(minVal, maxVal, minInclusive, maxInclusive, p.Loc) + q, err := rp.generate() + if err != nil { + return nil, ParseError{rel.pos, err.Error()} + } + if ctx.field != "" { + if fieldable, ok := q.(query.FieldableQuery); ok { + fieldable.SetField(ctx.field) + } else { + return nil, ParseError{ctx.fieldPos, "unexpected field"} + } + } + return q, nil +} diff --git a/vendor/github.com/bcampbell/qs/range.go b/vendor/github.com/bcampbell/qs/range.go new file mode 100644 index 0000000..3bf7d23 --- /dev/null +++ b/vendor/github.com/bcampbell/qs/range.go @@ -0,0 +1,117 @@ +package qs + +import ( + "fmt" + "github.com/blevesearch/bleve" + "github.com/blevesearch/bleve/search/query" + "strconv" + "time" +) + +// helper stuff for setting up range queries + +// returns time, precision +// TODO: support other precisions? "YYYY-MM", "YYYY-MM-DDThh:mm:ss" etc? +func parseTime(in string, loc *time.Location) (time.Time, string) { + t, err := time.ParseInLocation("2006-01-02", in, loc) + if err != nil { + return time.Time{}, "" + } + return t, "day" +} + +type rangeParams struct { + min, max *string + minInclusive, maxInclusive *bool + // need location for parsing times + loc *time.Location +} + +func newRangeParams(minVal, maxVal string, minInc, maxInc bool, loc *time.Location) *rangeParams { + rp := &rangeParams{} + if minVal != "" { + rp.min = &minVal + rp.minInclusive = &minInc + } + if maxVal != "" { + rp.max = &maxVal + rp.maxInclusive = &maxInc + } + if loc == nil { + loc = time.UTC + } + rp.loc = loc + return rp +} + +func (rp *rangeParams) numericArgs() (bool, *float64, *float64) { + var f1, f2 *float64 + if rp.min != nil { + f, err := strconv.ParseFloat(*rp.min, 64) + if err != nil { + return false, nil, nil + } + f1 = &f + } + if rp.max != nil { + f, err := strconv.ParseFloat(*rp.max, 64) + if err != nil { + return false, nil, nil + } + f2 = &f + } + return true, f1, f2 +} + +func (rp *rangeParams) dateArgs() (bool, time.Time, time.Time) { + var truthy bool = true + var falsey bool = false + var t1, t2 time.Time + var prec string + if rp.min != nil { + t1, prec = parseTime(*rp.min, rp.loc) + switch prec { + case "day": + if !*rp.minInclusive { + // add 1 day and make inclusive + t1 = t1.AddDate(0, 0, 1) + rp.minInclusive = &truthy + } + default: + return false, t1, t2 + } + } + if rp.max != nil { + t2, prec = parseTime(*rp.max, rp.loc) + switch prec { + case "day": + if *rp.maxInclusive { + // add 1 day and change to exclusive + t2 = t2.AddDate(0, 0, 1) + rp.maxInclusive = &falsey + } + default: + return false, t1, t2 + } + } + + return true, t1, t2 +} + +// try and build a query from the given params +func (rp *rangeParams) generate() (query.Query, error) { + if rp.min == nil && rp.max == nil { + return nil, fmt.Errorf("empty range") + } + isNumeric, f1, f2 := rp.numericArgs() + if isNumeric { + return bleve.NewNumericRangeInclusiveQuery(f1, f2, rp.minInclusive, rp.maxInclusive), nil + } + + isDate, t1, t2 := rp.dateArgs() + if isDate { + return bleve.NewDateRangeInclusiveQuery(t1, t2, rp.minInclusive, rp.maxInclusive), nil + } + return nil, fmt.Errorf("not numeric") + +} diff --git a/vendor/github.com/bcampbell/qs/syntax.md b/vendor/github.com/bcampbell/qs/syntax.md new file mode 100644 index 0000000..fbfd813 --- /dev/null +++ b/vendor/github.com/bcampbell/qs/syntax.md @@ -0,0 +1,202 @@ +# Query String Syntax + +## Basics + +Single words are treated as simple terms. The results will include +documents containing those terms, eg: + + embiggen + cromulent + +Terms are separated by spaces, so: + + navel orange + +Will match both documents containing "navel" and documents containing "orange", +which is probably not what was intended. The user was probably looking for +documents about "navel oranges" rather than those about "orange navels". + +To search for phrases - multiple words, matched in order - enclose them in double quotes. + +For example: + + "navel orange" + +## Boolean Operators + +### `OR` + +The `OR` operator returns documents matched by terms on either side of it. +It is the default operator, meaning that the following two queries are +considered to be equivalent: + + orange OR "navel orange" + orange "navel orange" + +Note that the query parser can be set up to use `AND` as the default operator instead of `OR`. + +### `AND` + +'`AND`' returns documents which match the terms on *both* sides. + + orange AND citrus + +### `+` + +'`+`' is the 'required' operator. A term prefixed with '`+`' must exist in matching +documents. + +For example, to return documents definitely containing "orange" and maybe also +containing "fruit": + + fruit +orange + + +### `NOT` + +`NOT` excludes documents that match the following term. + +For example, to match documents containing "orange" but not "paint": + + orange NOT paint + + +### `-` + +Prefixing a term with '`-`' will "prohibit" it. + +For example, to match documents containing "orange" but not "paint": + + orange -paint + + +### Precedence + +`-`, `+` and `NOT` take precedence over `AND`, which takes precedence over `OR`. + +For example, + + lemon OR orange AND citrus + +is treated as: + + lemon OR (orange AND citrus) + + + +## Field Scoping + +You can control which fields are matched by prefixing the name of a field separated by a colon. + +Examples: + + genus:citrus + headline:"How to Make the Perfect Negroni" + tags:(fruit OR paint) + +If no field is specified, the default `_all` is used, unless overridden in the index mapping. + + +## Grouping + +Parentheses can be used to group sub queries. +For example: + + (shaddock OR pomelo OR pamplemousse) AND (family:rutaceae AND NOT genus:fortunella) AND colour:(greenish OR yellowish) + +## Wildcards + +Within an individual term, partial matches can be described using wildcard characters: + +`?` to match any single character + +`*` To match and sequence of zero or more characters + +For example: + + qu?ck bro* + + +## Fuzziness + +A fuzzy query is a term query that matches terms within a given [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance). +The edit distance is the number of single-character edits (insertions, deletions or substitutions) allowed. + +To specify a fuzzy query, use the tilde sign (`~`), optionally followed by the edit distance you'll accept. + + +For example, + + colour~1 + +to match "`colour`" or "`color`" (or "`zolour`" or "`colours`"... but not "`colors`", as the `~1` allows only a single character change). + +If no number is specified, the default value is 1, so the following are equivalent: + + wibble~ + wibble~1 + + + +## Boosting + +Boosting a term doesn't affect the set of matching documents, but it does +indicate that that term should be assigned greater importance when the +results are being scored by relevance. + +To boost a term, you use the boost operator '`^`' followed by a number +indicating the relative importance of the term. + +For example, in the following query, "grapefruit" should be considered twice +as important as "lime": + + grapefruit^2 lime + +You can boost more complex sub queries: + + (grapefruit OR orange)^2 "navel orange"^4 genus:citrus + + + +## Ranges + +Inclusive ranges can be described with square braces and `TO`, eg: + + num:[1 TO 5] + date:[2010-01-01 TO 2010-01-31] + +Dates must be in `YYYY-MM-DD` form. + +Exclusive ranges are supported using curly braces. So, these are equivalent: + + score:{0 TO 10} + score:[1 TO 9] + + + +You can mix inclusive and exclusive endpoints, eg: + + pubdate:[2010-01-01 TO 2011-01-01} + shoesize:{0 TO 16] + byte:[0 TO 256} + +You can have open ranges by leaving off either endpoint: + + pubdate:[2000-01-01 TO ] + temp:[TO 100} + + +NOTE: ranges currently work only on numeric and date fields. + + +## Relational Operators + +You can perform numeric comparisons using the `>`, `>=`, `<`, and `<=` operators. +These are equivalent to using the above range syntax with unbounded ranges. + +For example: + + score:>=100 + score:[ TO 100] + + diff --git a/vendor/modules.txt b/vendor/modules.txt index ea98e37..72f57bb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,16 +4,19 @@ git.giftfish.de/ston1th/godrop/v2 git.giftfish.de/ston1th/jwt/v3 # github.com/RoaringBitmap/roaring v0.4.17 github.com/RoaringBitmap/roaring +# github.com/bcampbell/qs v0.0.0-20161004022730-15caa99abd01 +github.com/bcampbell/qs # github.com/blevesearch/bleve v0.8.0 github.com/blevesearch/bleve github.com/blevesearch/bleve/analysis/analyzer/custom github.com/blevesearch/bleve/analysis/token/edgengram github.com/blevesearch/bleve/analysis/token/lowercase github.com/blevesearch/bleve/analysis/tokenizer/unicode +github.com/blevesearch/bleve/document github.com/blevesearch/bleve/mapping +github.com/blevesearch/bleve/search/query github.com/blevesearch/bleve/analysis github.com/blevesearch/bleve/analysis/datetime/optional -github.com/blevesearch/bleve/document github.com/blevesearch/bleve/index github.com/blevesearch/bleve/index/scorch github.com/blevesearch/bleve/index/store @@ -26,19 +29,18 @@ github.com/blevesearch/bleve/search/collector github.com/blevesearch/bleve/search/facet github.com/blevesearch/bleve/search/highlight github.com/blevesearch/bleve/search/highlight/highlighter/html -github.com/blevesearch/bleve/search/query github.com/blevesearch/bleve/size -github.com/blevesearch/bleve/analysis/analyzer/standard github.com/blevesearch/bleve/geo -github.com/blevesearch/bleve/analysis/datetime/flexible github.com/blevesearch/bleve/numeric +github.com/blevesearch/bleve/analysis/analyzer/standard +github.com/blevesearch/bleve/search/searcher +github.com/blevesearch/bleve/analysis/datetime/flexible github.com/blevesearch/bleve/index/scorch/mergeplan github.com/blevesearch/bleve/index/scorch/segment github.com/blevesearch/bleve/index/scorch/segment/zap github.com/blevesearch/bleve/search/highlight/format/html github.com/blevesearch/bleve/search/highlight/fragmenter/simple github.com/blevesearch/bleve/search/highlight/highlighter/simple -github.com/blevesearch/bleve/search/searcher github.com/blevesearch/bleve/analysis/lang/en github.com/blevesearch/bleve/search/scorer github.com/blevesearch/bleve/analysis/token/porter