diff --git a/.gitignore b/.gitignore index c2c7973a..63612403 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ /utils/bleve_registry/bleve_registry /y.output *.test +tags diff --git a/analysis/type.go b/analysis/type.go index c5f88ffd..936fbd62 100644 --- a/analysis/type.go +++ b/analysis/type.go @@ -28,6 +28,7 @@ const ( Shingle Single Double + Boolean ) // Token represents one occurrence of a term at a particular location in a diff --git a/document/field_boolean.go b/document/field_boolean.go new file mode 100644 index 00000000..7fc200b0 --- /dev/null +++ b/document/field_boolean.go @@ -0,0 +1,93 @@ +// Copyright (c) 2014 Couchbase, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the +// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +// either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +package document + +import ( + "fmt" + + "github.com/blevesearch/bleve/analysis" +) + +const DefaultBooleanIndexingOptions = StoreField | IndexField + +type BooleanField struct { + name string + arrayPositions []uint64 + options IndexingOptions + value []byte +} + +func (b *BooleanField) Name() string { + return b.name +} + +func (b *BooleanField) ArrayPositions() []uint64 { + return b.arrayPositions +} + +func (b *BooleanField) Options() IndexingOptions { + return b.options +} + +func (b *BooleanField) Analyze() (int, analysis.TokenFrequencies) { + tokens := make(analysis.TokenStream, 0) + tokens = append(tokens, &analysis.Token{ + Start: 0, + End: len(b.value), + Term: b.value, + Position: 1, + Type: analysis.Boolean, + }) + + fieldLength := len(tokens) + tokenFreqs := analysis.TokenFrequency(tokens, b.arrayPositions, b.options.IncludeTermVectors()) + return fieldLength, tokenFreqs +} + +func (b *BooleanField) Value() []byte { + return b.value +} + +func (b *BooleanField) Boolean() (bool, error) { + if len(b.value) == 1 { + return b.value[0] == 'T', nil + } + return false, fmt.Errorf("boolean field has %d bytes", len(b.value)) +} + +func (b *BooleanField) GoString() string { + return fmt.Sprintf("&document.BooleanField{Name:%s, Options: %s, Value: %s}", b.name, b.options, b.value) +} + +func NewBooleanFieldFromBytes(name string, arrayPositions []uint64, value []byte) *BooleanField { + return &BooleanField{ + name: name, + arrayPositions: arrayPositions, + value: value, + options: DefaultNumericIndexingOptions, + } +} + +func NewBooleanField(name string, arrayPositions []uint64, b bool) *BooleanField { + return NewBooleanFieldWithIndexingOptions(name, arrayPositions, b, DefaultNumericIndexingOptions) +} + +func NewBooleanFieldWithIndexingOptions(name string, arrayPositions []uint64, b bool, options IndexingOptions) *BooleanField { + v := []byte("F") + if b { + v = []byte("T") + } + return &BooleanField{ + name: name, + arrayPositions: arrayPositions, + value: v, + options: options, + } +} diff --git a/index/firestorm/analysis.go b/index/firestorm/analysis.go index 58f105e3..878f2994 100644 --- a/index/firestorm/analysis.go +++ b/index/firestorm/analysis.go @@ -158,6 +158,8 @@ func encodeFieldType(f document.Field) byte { fieldType = 'n' case *document.DateTimeField: fieldType = 'd' + case *document.BooleanField: + fieldType = 'b' case *document.CompositeField: fieldType = 'c' } diff --git a/index/firestorm/reader.go b/index/firestorm/reader.go index 553ada99..29cc5f64 100644 --- a/index/firestorm/reader.go +++ b/index/firestorm/reader.go @@ -112,6 +112,8 @@ func (r *firestormReader) decodeFieldType(name string, pos []uint64, value []byt return document.NewNumericFieldFromBytes(name, pos, value[1:]) case 'd': return document.NewDateTimeFieldFromBytes(name, pos, value[1:]) + case 'b': + return document.NewBooleanFieldFromBytes(name, pos, value[1:]) } return nil } diff --git a/index/upside_down/upside_down.go b/index/upside_down/upside_down.go index ed5a3d6c..c24256b5 100644 --- a/index/upside_down/upside_down.go +++ b/index/upside_down/upside_down.go @@ -531,6 +531,8 @@ func encodeFieldType(f document.Field) byte { fieldType = 'n' case *document.DateTimeField: fieldType = 'd' + case *document.BooleanField: + fieldType = 'b' case *document.CompositeField: fieldType = 'c' } @@ -682,6 +684,8 @@ func decodeFieldType(typ byte, name string, pos []uint64, value []byte) document return document.NewNumericFieldFromBytes(name, pos, value) case 'd': return document.NewDateTimeFieldFromBytes(name, pos, value) + case 'b': + return document.NewBooleanFieldFromBytes(name, pos, value) } return nil } diff --git a/index_impl.go b/index_impl.go index cc481cc0..cee52ae2 100644 --- a/index_impl.go +++ b/index_impl.go @@ -486,6 +486,11 @@ func (i *indexImpl) Search(req *SearchRequest) (sr *SearchResult, err error) { if err == nil { value = datetime.Format(time.RFC3339) } + case *document.BooleanField: + boolean, err := docF.Boolean() + if err == nil { + value = boolean + } } if value != nil { hit.AddFieldValue(docF.Name(), value) diff --git a/index_test.go b/index_test.go index f300008b..8f6ff6ee 100644 --- a/index_test.go +++ b/index_test.go @@ -421,6 +421,8 @@ func TestStoredFieldPreserved(t *testing.T) { doca := map[string]interface{}{ "name": "Marty", "desc": "GopherCON India", + "bool": true, + "num": float64(1), } err = index.Index("a", doca) if err != nil { @@ -429,7 +431,7 @@ func TestStoredFieldPreserved(t *testing.T) { q := NewTermQuery("marty") req := NewSearchRequest(q) - req.Fields = []string{"name", "desc"} + req.Fields = []string{"name", "desc", "bool", "num"} res, err := index.Search(req) if err != nil { t.Error(err) @@ -438,14 +440,18 @@ func TestStoredFieldPreserved(t *testing.T) { if len(res.Hits) != 1 { t.Fatalf("expected 1 hit, got %d", len(res.Hits)) } - if res.Hits[0].Fields["name"] != "Marty" { t.Errorf("expected 'Marty' got '%s'", res.Hits[0].Fields["name"]) } if res.Hits[0].Fields["desc"] != "GopherCON India" { t.Errorf("expected 'GopherCON India' got '%s'", res.Hits[0].Fields["desc"]) } - + if res.Hits[0].Fields["num"] != float64(1) { + t.Errorf("expected '1' got '%v'", res.Hits[0].Fields["num"]) + } + if res.Hits[0].Fields["bool"] != true { + t.Errorf("expected 'true' got '%v'", res.Hits[0].Fields["bool"]) + } } func TestDict(t *testing.T) { @@ -1311,3 +1317,65 @@ func TestDocumentFieldArrayPositionsBug295(t *testing.T) { t.Fatal(err) } } + +func TestBooleanFieldMappingIssue109(t *testing.T) { + defer func() { + err := os.RemoveAll("testidx") + if err != nil { + t.Fatal(err) + } + }() + + m := NewIndexMapping() + m.DefaultMapping = NewDocumentMapping() + m.DefaultMapping.AddFieldMappingsAt("Bool", NewBooleanFieldMapping()) + + index, err := New("testidx", m) + if err != nil { + t.Fatal(err) + } + + type doc struct { + Bool bool + } + err = index.Index("true", &doc{Bool: true}) + if err != nil { + t.Fatal(err) + } + err = index.Index("false", &doc{Bool: false}) + if err != nil { + t.Fatal(err) + } + + sreq := NewSearchRequest(NewBoolFieldQuery(true).SetField("Bool")) + sres, err := index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 results, got %d", sres.Total) + } + + sreq = NewSearchRequest(NewBoolFieldQuery(false).SetField("Bool")) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 results, got %d", sres.Total) + } + + sreq = NewSearchRequest(NewBoolFieldQuery(true)) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 results, got %d", sres.Total) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} diff --git a/mapping_document.go b/mapping_document.go index 6623c4ae..4ab0855f 100644 --- a/mapping_document.go +++ b/mapping_document.go @@ -67,7 +67,7 @@ func (dm *DocumentMapping) validate(cache *registry.Cache) error { } } switch field.Type { - case "text", "datetime", "number": + case "text", "datetime", "number", "boolean": default: return fmt.Errorf("unknown field type: '%s'", field.Type) } @@ -352,6 +352,18 @@ func (dm *DocumentMapping) processProperty(property interface{}, path []string, fieldMapping := newNumericFieldMappingDynamic() fieldMapping.processFloat64(propertyValFloat, pathString, path, indexes, context) } + case reflect.Bool: + propertyValBool := propertyValue.Bool() + if subDocMapping != nil { + // index by explicit mapping + for _, fieldMapping := range subDocMapping.Fields { + fieldMapping.processBoolean(propertyValBool, pathString, path, indexes, context) + } + } else if dm.Dynamic { + // automatic indexing behavior + fieldMapping := newBooleanFieldMappingDynamic() + fieldMapping.processBoolean(propertyValBool, pathString, path, indexes, context) + } case reflect.Struct: switch property := property.(type) { case time.Time: diff --git a/mapping_field.go b/mapping_field.go index 26914526..e3c4ae6b 100644 --- a/mapping_field.go +++ b/mapping_field.go @@ -100,6 +100,23 @@ func newDateTimeFieldMappingDynamic() *FieldMapping { return rv } +// NewBooleanFieldMapping returns a default field mapping for booleans +func NewBooleanFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "boolean", + Store: true, + Index: true, + IncludeInAll: true, + } +} + +func newBooleanFieldMappingDynamic() *FieldMapping { + rv := NewBooleanFieldMapping() + rv.Store = StoreDynamic + rv.Index = IndexDynamic + return rv +} + // Options returns the indexing options for this field. func (fm *FieldMapping) Options() document.IndexingOptions { var rv document.IndexingOptions @@ -171,6 +188,19 @@ func (fm *FieldMapping) processTime(propertyValueTime time.Time, pathString stri } } +func (fm *FieldMapping) processBoolean(propertyValueBool bool, pathString string, path []string, indexes []uint64, context *walkContext) { + fieldName := getFieldName(pathString, path, fm) + if fm.Type == "boolean" { + options := fm.Options() + field := document.NewBooleanFieldWithIndexingOptions(fieldName, indexes, propertyValueBool, options) + context.doc.AddField(field) + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } + } +} + func (fm *FieldMapping) analyzerForField(path []string, context *walkContext) *analysis.Analyzer { analyzerName := fm.Analyzer if analyzerName == "" { diff --git a/mapping_test.go b/mapping_test.go index ba276071..9cb34065 100644 --- a/mapping_test.go +++ b/mapping_test.go @@ -357,5 +357,48 @@ func TestEnablingDisablingStoringDynamicFields(t *testing.T) { t.Errorf("expected field 'name' to be not stored, is") } } - +} + +func TestMappingBool(t *testing.T) { + boolMapping := NewBooleanFieldMapping() + docMapping := NewDocumentMapping() + docMapping.AddFieldMappingsAt("prop", boolMapping) + mapping := NewIndexMapping() + mapping.AddDocumentMapping("doc", docMapping) + + pprop := false + x := struct { + Prop bool `json:"prop"` + PProp *bool `json:"pprop"` + }{ + Prop: true, + PProp: &pprop, + } + + doc := document.NewDocument("1") + err := mapping.mapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + foundProp := false + foundPProp := false + count := 0 + for _, f := range doc.Fields { + if f.Name() == "prop" { + foundProp = true + } + if f.Name() == "pprop" { + foundPProp = true + } + count++ + } + if !foundProp { + t.Errorf("expected to find bool field named 'prop'") + } + if !foundPProp { + t.Errorf("expected to find pointer to bool field named 'pprop'") + } + if count != 2 { + t.Errorf("expected to find 1 find, found %d", count) + } } diff --git a/query_bool_field.go b/query_bool_field.go new file mode 100644 index 00000000..ed3ac85f --- /dev/null +++ b/query_bool_field.go @@ -0,0 +1,64 @@ +// Copyright (c) 2014 Couchbase, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the +// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +// either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +package bleve + +import ( + "github.com/blevesearch/bleve/index" + "github.com/blevesearch/bleve/search" + "github.com/blevesearch/bleve/search/searchers" +) + +type boolFieldQuery struct { + Bool bool `json:"bool"` + FieldVal string `json:"field,omitempty"` + BoostVal float64 `json:"boost,omitempty"` +} + +// NewBoolFieldQuery creates a new Query for boolean fields +func NewBoolFieldQuery(val bool) *boolFieldQuery { + return &boolFieldQuery{ + Bool: val, + BoostVal: 1.0, + } +} + +func (q *boolFieldQuery) Boost() float64 { + return q.BoostVal +} + +func (q *boolFieldQuery) SetBoost(b float64) Query { + q.BoostVal = b + return q +} + +func (q *boolFieldQuery) Field() string { + return q.FieldVal +} + +func (q *boolFieldQuery) SetField(f string) Query { + q.FieldVal = f + return q +} + +func (q *boolFieldQuery) Searcher(i index.IndexReader, m *IndexMapping, explain bool) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultField + } + term := "F" + if q.Bool { + term = "T" + } + return searchers.NewTermSearcher(i, term, field, q.BoostVal, explain) +} + +func (q *boolFieldQuery) Validate() error { + return nil +}