Scondary Index Layer

positive is a package that provides a Secondary Index layer above badger.

Arbitrary indices can be defined using Go language itself. And indices can be queried inside current transaction.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/dgraph-io/badger/v4"
    "github.com/dc0d/positive/pkg/layer"
)

// Comment represents a sample data structure to store and index
type Comment struct {
    ID   string    `json:"id"`
    By   string    `json:"by"`
    Text string    `json:"text"`
    At   time.Time `json:"at"`
    Tags []string  `json:"tags"`
}

// createDB initializes a Badger database
func createDB(dir string) *badger.DB {
    opts := badger.DefaultOptions(dir)
    db, err := badger.Open(opts)
    if err != nil {
        log.Fatalf("Failed to open Badger DB: %v", err)
    }
    return db
}

// main demonstrates setting up indices, inserting data, and querying
func main() {
    // Initialize Badger DB
    db := createDB("./data")
    defer db.Close()

    // Define secondary indices using Positive
    indexTags := layer.NewIndex("tags", func(key, val []byte) (entries []layer.IndexEntry, err error) {
        var c Comment
        if err := json.Unmarshal(val, &c); err != nil {
            return nil, err
        }
        for _, tag := range c.Tags {
            entries = append(entries, layer.IndexEntry{Index: []byte(tag)})
        }
        return entries, nil
    })

    indexBy := layer.NewIndex("by", func(key, val []byte) (entries []layer.IndexEntry, err error) {
        var c Comment
        if err := json.Unmarshal(val, &c); err != nil {
            return nil, err
        }
        if c.By != "" {
            entries = append(entries, layer.IndexEntry{Index: []byte(c.By)})
        }
        return entries, nil
    })

    // Index builder function for transactions
    sampleIndexBuilder := func(txn *layer.Txn, entries map[string][]byte) error {
        for k, v := range entries {
            if err := layer.Emit(txn, indexTags, []byte(k), v); err != nil {
                return err
            }
            if err := layer.Emit(txn, indexBy, []byte(k), v); err != nil {
                return err
            }
        }
        return nil
    }

    // Insert sample data
    comment := Comment{
        ID:   "CMNT::001",
        By:   "Frodo Baggins",
        Text: "Hi there!",
        At:   time.Now(),
        Tags: []string{"tech", "golang"},
    }
    js, err := json.Marshal(comment)
    if err != nil {
        log.Fatalf("Failed to marshal comment: %v", err)
    }

    txn := db.NewTransaction(true)
    defer txn.Discard()

    if err := txn.Set([]byte(comment.ID), js); err != nil {
        log.Fatalf("Failed to set data: %v", err)
    }

    if err := txn.CommitWith(sampleIndexBuilder, nil); err != nil {
        log.Fatalf("Failed to commit transaction: %v", err)
    }
    fmt.Println("Data inserted successfully")

    // Query by tag
    queryByTag(db, indexTags, "golang")

    // Query by author
    queryByAuthor(db, indexBy, "Frodo Baggins")
}

// queryByTag queries comments by a specific tag
func queryByTag(db *badger.DB, index *layer.Index, tag string) {
    txn := layer.NewTxn(db.NewTransaction(false))
    defer txn.Discard()

    params := layer.Q{
        IndexName: index.Name(),
        Value:     []byte(tag),
    }
    results, count, err := layer.QueryIndex(params, txn)
    if err != nil {
        log.Printf("Failed to query by tag: %v", err)
        return
    }

    fmt.Printf("Found %d comments with tag '%s':\n", count, tag)
    for _, res := range results {
        var c Comment
        if err := json.Unmarshal(res.Val, &c); err != nil {
            log.Printf("Failed to unmarshal result: %v", err)
            continue
        }
        fmt.Printf("- %s by %s: %s\n", c.ID, c.By, c.Text)
    }
}

// queryByAuthor queries comments by a specific author
func queryByAuthor(db *badger.DB, index *layer.Index, author string) {
    txn := layer.NewTxn(db.NewTransaction(false))
    defer txn.Discard()

    params := layer.Q{
        IndexName: index.Name(),
        Value:     []byte(author),
    }
    results, count, err := layer.QueryIndex(params, txn)
    if err != nil {
        log.Printf("Failed to query by author: %v", err)
        return
    }

    fmt.Printf("Found %d comments by '%s':\n", count, author)
    for _, res := range results {
        var c Comment
        if err := json.Unmarshal(res.Val, &c); err != nil {
            log.Printf("Failed to unmarshal result: %v", err)
            continue
        }
        fmt.Printf("- %s: %s (Tags: %v)\n", c.ID, c.Text, c.Tags)
    }
}