From 1daaa1617269e646bc30e17b61610639b27a1743 Mon Sep 17 00:00:00 2001 From: Josh Baker Date: Thu, 18 Aug 2016 09:08:30 -0700 Subject: [PATCH] multi value indexes --- README.md | 36 ++++++++++++++ buntdb.go | 25 ++++++++-- buntdb_test.go | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 60890b3..c4ce5ff 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Features - [Spatial indexing](#spatial-indexes) for up to 20 dimensions; Useful for Geospatial data - Index fields inside [JSON](#json-indexes) documents - Create [custom indexes](#custom-indexes) for any data type +- Support for [multi value indexes](#multi-value-index); Similar to a SQL multi column index - [Built-in types](#built-in-types) that are easy to get up & running; String, Uint, Int, Float - Flexible [iteration](#iterating) of data; ascending, descending, and ranges - [Durable append-only file](#append-only-file) format for persistence. @@ -411,6 +412,41 @@ Order by age range 30-50 1: {"name":{"first":"Tom","last":"Johnson"},"age":38} 2: {"name":{"first":"Janet","last":"Prichard"},"age":47} ``` +## Multi Value Index +With BuntDB it's possible to join multiple values on a single index. +This is similar to a [multi column index](http://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html) in a traditional SQL database. + +In this example we are creating a multi value index on "name.last" and "age": + +```go + db, _ := buntdb.Open(":memory:") + db.CreateIndex("last_name_age", "*", buntdb.IndexJSON("name.last"), buntdb.IndexJSON("age")) + db.Update(func(tx *buntdb.Tx) error { + tx.Set("1", `{"name":{"first":"Tom","last":"Johnson"},"age":38}`, nil) + tx.Set("2", `{"name":{"first":"Janet","last":"Prichard"},"age":47}`, nil) + tx.Set("3", `{"name":{"first":"Carol","last":"Anderson"},"age":52}`, nil) + tx.Set("4", `{"name":{"first":"Alan","last":"Cooper"},"age":28}`, nil) + tx.Set("5", `{"name":{"first":"Sam","last":"Anderson"},"age":51}`, nil) + tx.Set("6", `{"name":{"first":"Melinda","last":"Prichard"},"age":44}`, nil) + return nil + }) + db.View(func(tx *buntdb.Tx) error { + tx.Ascend("last_name_age", func(key, value string) bool { + fmt.Printf("%s: %s\n", key, value) + return true + }) + return nil + }) + + // Output: + // 5: {"name":{"first":"Sam","last":"Anderson"},"age":51} + // 3: {"name":{"first":"Carol","last":"Anderson"},"age":52} + // 4: {"name":{"first":"Alan","last":"Cooper"},"age":28} + // 1: {"name":{"first":"Tom","last":"Johnson"},"age":38} + // 6: {"name":{"first":"Melinda","last":"Prichard"},"age":44} + // 2: {"name":{"first":"Janet","last":"Prichard"},"age":47} +``` + ## Data Expiration Items can be automatically evicted by using the `SetOptions` object in the `Set` function to set a `TTL`. diff --git a/buntdb.go b/buntdb.go index 4858d58..ca72209 100644 --- a/buntdb.go +++ b/buntdb.go @@ -89,7 +89,7 @@ const ( // This is the recommended setting. EverySecond = 1 // Always is used to sync data after every write to disk. - // Very very slow. Very safe. + // Slow. Very safe. Always = 2 ) @@ -202,7 +202,7 @@ type index struct { // There are some default less function that can be used such as // IndexString, IndexBinary, etc. func (db *DB) CreateIndex(name, pattern string, - less func(a, b string) bool) error { + less ...func(a, b string) bool) error { return db.createIndex(name, pattern, less, nil) } @@ -229,7 +229,7 @@ func (db *DB) CreateSpatialIndex(name, pattern string, func (db *DB) createIndex( name string, pattern string, - less func(a, b string) bool, + lessers []func(a, b string) bool, rect func(item string) (min, max []float64), ) error { db.mu.Lock() @@ -243,6 +243,25 @@ func (db *DB) createIndex( if _, ok := db.idxs[name]; ok { return ErrIndexExists } + var less func(a, b string) bool + switch len(lessers) { + default: + less = func(a, b string) bool { + for i := 0; i < len(lessers)-1; i++ { + if lessers[i](a, b) { + return true + } + if lessers[i](b, a) { + return false + } + } + return lessers[len(lessers)-1](a, b) + } + case 0: + less = func(a, b string) bool { return false } + case 1: + less = lessers[0] + } idx := &index{ name: name, pattern: pattern, diff --git a/buntdb_test.go b/buntdb_test.go index f47a3a5..1343687 100644 --- a/buntdb_test.go +++ b/buntdb_test.go @@ -414,6 +414,138 @@ func TestVariousTx(t *testing.T) { t.Fatalf("should not be able to perform transactionso on a closed database.") } } +func ExampleDB_CreateIndex_jSON() { + db, _ := Open(":memory:") + db.CreateIndex("last_name", "*", IndexJSON("name.last")) + db.CreateIndex("age", "*", IndexJSON("age")) + db.Update(func(tx *Tx) error { + tx.Set("1", `{"name":{"first":"Tom","last":"Johnson"},"age":38}`, nil) + tx.Set("2", `{"name":{"first":"Janet","last":"Prichard"},"age":47}`, nil) + tx.Set("3", `{"name":{"first":"Carol","last":"Anderson"},"age":52}`, nil) + tx.Set("4", `{"name":{"first":"Alan","last":"Cooper"},"age":28}`, nil) + return nil + }) + db.View(func(tx *Tx) error { + fmt.Println("Order by last name") + tx.Ascend("last_name", func(key, value string) bool { + fmt.Printf("%s: %s\n", key, value) + return true + }) + fmt.Println("Order by age") + tx.Ascend("age", func(key, value string) bool { + fmt.Printf("%s: %s\n", key, value) + return true + }) + fmt.Println("Order by age range 30-50") + tx.AscendRange("age", `{"age":30}`, `{"age":50}`, func(key, value string) bool { + fmt.Printf("%s: %s\n", key, value) + return true + }) + return nil + }) + + // Output: + // Order by last name + // 3: {"name":{"first":"Carol","last":"Anderson"},"age":52} + // 4: {"name":{"first":"Alan","last":"Cooper"},"age":28} + // 1: {"name":{"first":"Tom","last":"Johnson"},"age":38} + // 2: {"name":{"first":"Janet","last":"Prichard"},"age":47} + // Order by age + // 4: {"name":{"first":"Alan","last":"Cooper"},"age":28} + // 1: {"name":{"first":"Tom","last":"Johnson"},"age":38} + // 2: {"name":{"first":"Janet","last":"Prichard"},"age":47} + // 3: {"name":{"first":"Carol","last":"Anderson"},"age":52} + // Order by age range 30-50 + // 1: {"name":{"first":"Tom","last":"Johnson"},"age":38} + // 2: {"name":{"first":"Janet","last":"Prichard"},"age":47} +} + +func ExampleDB_CreateIndex_strings() { + db, _ := Open(":memory:") + db.CreateIndex("name", "*", IndexString) + db.Update(func(tx *Tx) error { + tx.Set("1", "Tom", nil) + tx.Set("2", "Janet", nil) + tx.Set("3", "Carol", nil) + tx.Set("4", "Alan", nil) + tx.Set("5", "Sam", nil) + tx.Set("6", "Melinda", nil) + return nil + }) + db.View(func(tx *Tx) error { + tx.Ascend("name", func(key, value string) bool { + fmt.Printf("%s: %s\n", key, value) + return true + }) + return nil + }) + + // Output: + //4: Alan + //3: Carol + //2: Janet + //6: Melinda + //5: Sam + //1: Tom +} + +func ExampleDB_CreateIndex_ints() { + db, _ := Open(":memory:") + db.CreateIndex("age", "*", IndexInt) + db.Update(func(tx *Tx) error { + tx.Set("1", "30", nil) + tx.Set("2", "51", nil) + tx.Set("3", "16", nil) + tx.Set("4", "76", nil) + tx.Set("5", "23", nil) + tx.Set("6", "43", nil) + return nil + }) + db.View(func(tx *Tx) error { + tx.Ascend("age", func(key, value string) bool { + fmt.Printf("%s: %s\n", key, value) + return true + }) + return nil + }) + + // Output: + //3: 16 + //5: 23 + //1: 30 + //6: 43 + //2: 51 + //4: 76 +} +func ExampleDB_CreateIndex_multipleFields() { + db, _ := Open(":memory:") + db.CreateIndex("last_name_age", "*", IndexJSON("name.last"), IndexJSON("age")) + db.Update(func(tx *Tx) error { + tx.Set("1", `{"name":{"first":"Tom","last":"Johnson"},"age":38}`, nil) + tx.Set("2", `{"name":{"first":"Janet","last":"Prichard"},"age":47}`, nil) + tx.Set("3", `{"name":{"first":"Carol","last":"Anderson"},"age":52}`, nil) + tx.Set("4", `{"name":{"first":"Alan","last":"Cooper"},"age":28}`, nil) + tx.Set("5", `{"name":{"first":"Sam","last":"Anderson"},"age":51}`, nil) + tx.Set("6", `{"name":{"first":"Melinda","last":"Prichard"},"age":44}`, nil) + return nil + }) + db.View(func(tx *Tx) error { + tx.Ascend("last_name_age", func(key, value string) bool { + fmt.Printf("%s: %s\n", key, value) + return true + }) + return nil + }) + + // Output: + //5: {"name":{"first":"Sam","last":"Anderson"},"age":51} + //3: {"name":{"first":"Carol","last":"Anderson"},"age":52} + //4: {"name":{"first":"Alan","last":"Cooper"},"age":28} + //1: {"name":{"first":"Tom","last":"Johnson"},"age":38} + //6: {"name":{"first":"Melinda","last":"Prichard"},"age":44} + //2: {"name":{"first":"Janet","last":"Prichard"},"age":47} +} + func TestNoExpiringItem(t *testing.T) { item := &dbItem{key: "key", val: "val"} if !item.expiresAt().Equal(maxTime) {