True love is not hard to find with RedisJSON

femolacaster

femolacaster

Posted on October 25, 2022

True love is not hard to find with RedisJSON

In the first episode of this series, we looked at the importance of JSON, JSON databases, and RedisJSON, installing Redis Cloud, Redis Stack, and Redis Insight, and how we can store all types of data(scalar, object, array of objects) in RedisJSON. Make out some time to read that great article here if you haven’t. No doubt, we were getting an inch closer to our goal of finding perfect matches for returning inmates. Everyone could find true love after all. Let’s take a step further toward our goal in this article.

Yay!!! It’s time to create!

The wonders of RedisJSON would further be explored in this next tutorial. How can we prepare our data dimensions for our matches using code? With Golang, we would explore how to interact smoothly with our RedisJSON database and allow returning inmates to specify their interests in a breeze.

Cool stuff, yeah?!

If you are excited already, can I get an upvote? ❤️

We would be using a simple directory structure and code arrangement pattern typically in this post (as much as we can). It is recommended to use more Golang idiomatic architectural styles in more serious implementation. We would however separate concerns in the simplest of forms. We could expand on this pattern in a future post. We would also be using the REST API standard. This code would be built as a monolith to avoid complexities but can be scaled to much more advanced architectures later. To the micro-services and best practices Lords:

Relax a bit

Let’s make a directory for our code. In UNIX-like systems, we can do:

mkdir dating-app && cd dating-app
Enter fullscreen mode Exit fullscreen mode

It'd be great starting with setting and tidying up some of our dependencies. Run this in your terminal’s root project:

#go mod init {your-repo-name}
#For me I have:
go mod init github.com/femolacaster/dating-app

#Tidy things up
go mod tidy

#Call on your Redis Soldiers
go get github.com/gomodule/redigo/redis
go get github.com/nitishm/go-rejson/v4

#Let us include MUX for our API routing
go get -u github.com/gorilla/mux
Enter fullscreen mode Exit fullscreen mode

The next step would be to create the following routes in a folder named routes in our application’s root directory:

[route-dir]/routes/routes.go

package routes

import (
    "github.com/femolacaster/dating-app/controllers"
    "github.com/gorilla/mux"
)

func Init() *mux.Router {
    route := mux.NewRouter()

    route.HandleFunc("/api/v1/criteria", controllers.ShowAll)
    route.HandleFunc("/api/v1/criteria", controllers.Add).Methods("POST")
    route.HandleFunc("/api/v1/criteria/ {id}/dimension", controllers.ShowDimension)
    return route
}
Enter fullscreen mode Exit fullscreen mode

A simple routing is shown in the code above. The Init function returns 3 exported routes that would allow for the new addition of Criteria for returning Inmates which uses the POST method, display all the various dating criteria of Inmates on the application, and returns the dimension of particular criteria (either Casual or Serious) using the GET method.

A good next step would be to create helpers for our code. Helpers are functions that you use repeatedly throughout your code. They come through 😊. Two helper functions identified in this case are “RenderErrorResponse” and “RenderResponse “respectively. These functions help to render the output of our API in a simple format depending on whether it is an error or otherwise.

What we have in:

[route-dir]/helpers/dating.go

package helpers

import (
    "encoding/json"
    "net/http"
)

type ErrorResponse struct {
    Error string `json:"error"`
}

func RenderErrorResponse(w http.ResponseWriter, msg string, status int) {
    RenderResponse(w, ErrorResponse{Error: msg}, status)
}

func RenderResponse(w http.ResponseWriter, res interface{}, status int) {
    w.Header().Set("Content-Type", "application/json")
    content, err := json.Marshal(res)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    w.WriteHeader(status)
    if _, err = w.Write(content); err != nil {
    }
}
Enter fullscreen mode Exit fullscreen mode

In short, we can add one more helper function. All it does is connect to our RedisJSON local database and output the Redigo client connection instance which we can use for our logic:

func NewRedisConn() *rejson.Handler {
    var addr = flag.String("Server", "localhost:6379", "Redis server address")
    rh := rejson.NewReJSONHandler()
    flag.Parse()
    // Redigo Client
    conn, err := redis.Dial("tcp", *addr)
    if err != nil {
        log.Fatalf("Failed to connect to redis-server @ %s", *addr)
    }
    defer func() {
        _, err = conn.Do("FLUSHALL")
        err = conn.Close()
        if err != nil {
            log.Fatalf("Failed to communicate to redis-server @ %v", err)
        }
    }()
    rh.SetRedigoClient(conn)
    return rh
}
Enter fullscreen mode Exit fullscreen mode

Let us create the logic for our routes.

We create a new file:

[route-dir]/controllers/dating.go

This file would have three functions that define our logic. The first would allow for the new addition of Criteria for returning Inmates, the second would display all the various dating criteria of Inmates on the application and the last would allow filtering by criteria (either Casual or Serious).

The first thing to do in this section would be to store the various interest in a struct and then embody the interest and other details to form an Inmate’s criteria as shown in this struct:

type Criteria struct {
    ID                int             `json:"id"`
    Name              string          `json:"name"`
    Height            float32         `json:"height"` //height in feet and inches
    WeightKG          int             `json:"weight"`
    SexualOrientation string          `json:"sexualOrientation"`
    Age               int             `json:"age"`
    CasualInterest    CasualInterest  `json:"casualInterest"`
    SeriousInterest   SeriousInterest `json:"seriousInterest"`
}
type SeriousInterest struct {
    Career        bool `json:"career"`
    Children      bool `json:"children "`
    Communication bool `json:"communication"`
    Humanity      bool `json:"humanity"`
    Investment    bool `json:"investment"`
    Marriage      bool `json:"marriage"`
    Religion      bool `json:"religion"`
    Politics      bool `json:"politics"`
}
type CasualInterest struct {
    Entertainment bool `json:"entertainment"`
    Gym           bool `json:"gym"`
    Jewellries    bool `json:"jewellries"`
    OneNight      bool `json:"oneNight"`
    Restaurant    bool `json:"restaurant"`
    Swimming      bool `json:"swimming"`
    Travel        bool `json:"travel"`
    Yolo          bool `json:"yolo"`
}
Enter fullscreen mode Exit fullscreen mode

In all our logic functions, we used the returned Golang rejson instance in the helpers.NewRedisConn function that will be used to communicate to our RedisJSON database.

rh := helpers.NewRedisConn()
Enter fullscreen mode Exit fullscreen mode

Rejson is a Redis module that implements ECMA-404, the JSON Data Interchange Standard as a native data type and allows storing, updating, and fetching of JSON values from Redis keys which also supports the two popular Golang clients: Redigo and go-redis.

Here are the differences between Redigo and go-redis to make your own informed choice:

Redigo Go-Redis
It is less type-safe It is more type-safe
It could be faster and easier to use It could be slower and may not be easier to use as Redigo
Do not use it if planning to scale your database to a high-available cluster Perfect for clustering. Perfecto!

So yeah, the choice is yours. In this post, we, of course, chose the easier option which is Redigo and you would be seeing its usage in the controller functions.

For our first function that adds criteria for an Inmate:

func Add(w http.ResponseWriter, r *http.Request) {
    var req Criteria
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        helpers.RenderErrorResponse(w, "invalid request", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()

    rh := helpers.NewRedisConn()

    res, err := rh.JSONSet("criteria", ".", &req)
    if err != nil {
        log.Fatalf("Failed to JSONSet")
        return
    }
    if res.(string) == "OK" {
        fmt.Printf("Success: %s\n", res)
        helpers.RenderResponse(w, helpers.ErrorResponse{Error: "Successfully inserted new Criteria to Database"}, http.StatusCreated)
    } else {
        fmt.Println("Failed to Set: ")
        helpers.RenderErrorResponse(w, "invalid request", http.StatusBadRequest)
    }
}
Enter fullscreen mode Exit fullscreen mode

The second endpoint that shows all criteria is shown below:

func ShowAll(w http.ResponseWriter, r *http.Request) {
    rh := helpers.NewRedisConn()
    criteriaJSON, err := redis.Bytes(rh.JSONGet("criteria", "."))
    if err != nil {
        log.Fatalf(("Failed to get JSON"))
        return
    }

    readCriteria := Criteria{}
    err = json.Unmarshal(criteriaJSON, &readCriteria)
    if err != nil {
        fmt.Printf("JSON Unmarshal Failed")
        helpers.RenderErrorResponse(w, "invalid request", http.StatusBadRequest)
    }
    fmt.Printf("Student read from RedisJSON:%#v\n", readCriteria)
    helpers.RenderResponse(w, helpers.ErrorResponse{Error: "Successful retrieval of criterias"}, http.StatusOK)

}
Enter fullscreen mode Exit fullscreen mode

Now for getting if an Inmate’s criteria are Casual or Serious, could you try implementing that yourself?

There are many ways to go about it.

A tip would be:

Get all criteria from RedisJSON just as shown in the ShowAll function but this time using the key which is the id to get those criteria. Then since the CasualInterest struct and SeriousInterest struct have fields that are bool, compare the two individual struct values to determine which has the most “1” or “true”. That way you can decide the Inmate who is tilted to looking for something serious or casual. That logic works, I guess 🤔. But of course, you could come up with much better logic.

That should be easy. Would be nice to drop some of your beautiful implementations in the comment section😀.

In the main.go on our root directory, we can create our server:

package main

import (
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/femolacaster/dating-app/routes"
    "github.com/ichtrojan/thoth"
    "github.com/joho/godotenv"
)


func main() {

    logger, thothErr := thoth.Init("log")
    if thothErr != nil {
        log.Fatal(thothErr)
    }

    //warning, error, log, trace, metrics
    if envLoadErr := godotenv.Load(); envLoadErr != nil {
        logger.Log(errors.New("There was a problem loading an environmental file. Please check file is present."))
        log.Fatal("Error:::There was a problem loading an environmental file. Please check file is present.")
    }

    appPort, appPortExist := os.LookupEnv("APPPORT")

    if !appPortExist {
        logger.Log(errors.New("There was no Port variable for the application in the env file"))
        log.Fatal("Error:::There was no Port variable for the application in the env file")
    }
    address := ":" + appPort

    srv := &http.Server{
        Handler:           routes.Init(),
        Addr:              address,
        ReadTimeout:       1 * time.Second,
        ReadHeaderTimeout: 1 * time.Second,
        WriteTimeout:      1 * time.Second,
        IdleTimeout:       1 * time.Second,
    }

    log.Println("Starting server", address)
    fmt.Println("Go to localhost:" + appPort + " to view application")

    log.Fatal(srv.ListenAndServe())

}
Enter fullscreen mode Exit fullscreen mode

So, let’s get our server up:

In your project root, run the code by running this command:

go run main.go
Enter fullscreen mode Exit fullscreen mode

That’s it! We have successfully set up a simple API for returning inmates to get their matches. How awesome?!

This means that any system can connect to it and make use of the database information in its means, style, etc.

Let us dig into that assertion further. Make sure you have your Redis database instance on and spring up RedisInsight to have a view into what is going on.

1) Consider a simple use case: MR Peter who was once an Inmate wishes to declare his astonishing profile showing that he has quite a lot of qualities and hopes someone would accept and love him for who he is. With our API, MR Peter can fulfill this need via a mobile client, an IoT device, his browser, etc. maybe by speaking, typing, etc. translated in this manner:

curl -X POST localhost :9000 /api/v1/criteria
   -H "Content-Type: application/json"
   -d ' {
    "id":DATIN00025,
    "name":"Mr Peter Griffin",
    "height":6.4,
    "weight":120,
    "sexualOrientation":"straight",
    "age":45,
    "casualInterest":{
       "entertainment":true,
       "gym":false,
       "jewellries":false,
       "oneNight":false,
       "restaurant":true,
       "swimming":false,
       "travel":false,
       "yolo":true
    },
    "seriousInterest":{
       "career":false,
       "children ":true,
       "communication":false,
       "humanity":false,
       "investment":false,
       "marriage":false,
       "religion":false,
       "politics":true
    }
 }
‘
Enter fullscreen mode Exit fullscreen mode

2) Another use case. Mrs. Lois desires to connect with someone who can understand her, who can understand what it means to be behind bars as she has also been in that situation. She needs that man with dripping masculinity and vigor. Calling our API through her client just as seen below does the magic to show her all men available for her selection:

curl localhost :9000 /api/v1/criteria
   -H "Accept: application/json"
Enter fullscreen mode Exit fullscreen mode

3) Miss Meg, wants both sides of the coin at a casual level. No strong strings attached. She probably wants to know whether a particular sweet match meets that need. She sees Peter Griffin’s profile earlier and wants to determine if he got some casual or serious vibes. Miss Meg presses a button on her mobile, and all her mobile has to do is to call our unimplemented showDimension endpoint for Mr. Peter Griffin to see whether he is a casual match in a similar call such as:

curl localhost :9000 /api/v1/criteria/ DATIN00025/dimension
   -H "Accept: application/json"
Enter fullscreen mode Exit fullscreen mode

As with these matches, Mr.Peter, Mrs. Lois and Miss. Meg have been sorted. So as many more using this wonderful API we built!

That’s it! We have been able to find the perfect matches with ease! If that ain’t magic, what then?

Now, ask yourself. Should you be a RedisJSON enthusiast?🤔

As we journey through the other series, exploring other good sides of Redis such as RediSearch in the next episodes, we would keep progressing with our idea of helping returning inmates find their true love. And maybe someday, this would be a means for them to reintegrate into society faster and better. See you in the next series.

Something great for you! If you enjoyed this article, click on the upvote icon❤️, and show some love❤️ too. Show that we share some interest already ❤️. Maybe we should date 😊. When the total number of upvotes❤️ gets to, should I say 200? I would also share the full source code on Github.

All right, love birds❤️! Enjoy😀! Upvote❤️! Comment💬! it’d go a long way.

Comment especially💬. Let’s put some ideas into this code. I know you have some great ideas there.

This post is in collaboration with Redis.

You can check the following references for ideas:

[Image Credits: Photo by Marcus Ganahl on Unsplash]

💖 💪 🙅 🚩
femolacaster
femolacaster

Posted on October 25, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related