What I built
I’ve built an application in Golang, which is able to find video content from all of the videos that have been uploaded to the application. It utilises the Google Cloud Video Intelligence API to detect the features (content) inside the video.
Category Submission:
I decided to submit my application into multiple categories. Below is a list of all of the categories that I would like to submit my application in, as well as description as why I think the app can be submited into listed category.
- Search No More: Main feature of the application is to perform a quick search on ALL of the uploaded videos. That’s why I used Atlas Search, as it provides quick setup and is querying super fast.
- Think Outside the JS Box: As an backend developer, it was my goal for quite some time now to learn Golang, but I was procrastinating with learning Go, that’s why decided that the best way to learn new programming language is to build something with it. That’s why I picked Golang as my API language.
- Google Cloud Superstar: The “brains” behind the Video Feature detection Google Cloud Video Intelligence API, which provides Golang SDK and enables the developer to utilise powerful Google Cloud ML engine to detect the video features.
App Link
Because I use Google Cloud Video Intelligence API, which is not free, I did not want to post my application publicly and create some “unwanted” costs on my Google Cloud bill 😃. However, I recorded a short demo video, as well as screenshoted the application. After all, if you would like to try my app, you can always clone Git repo, add your Google Cloud credentials, and run the API.
Video
Screenshots
Background
My main motivation to build this application was to learn a few things:
- learn how to implement MongoDB into application using SDKs and libraries, and also connect Atlas Search
- learn how Google Cloud Video Intelligence works
- learn new programming language (Golang)
How I built it
I started my project with developing API server in Golang. For the HTTP library I used Gin framework, as it provides an friendly interface to build REST ready API.
For my application, two endpoints were enough. One was used for video upload, and one for video search. This is how endpoints are defined in Gin:
func main() {
r := gin.Default()
r.POST("/videos", Controllers.VideoStore)
r.GET("/videos", Controllers.VideoSearch)
err := r.Run()
if err != nil {
log.Fatalf(err.Error())
return
}
fmt.Println("Server running on :8080")
}
Video upload and feature detection
Video store method consists of the folowing steps:
File upload and transfer to Google Storage Buckets
User can upload the video to our API. Our API then uploads it to the Google Storage Bucket. This is the code that accomplishes that:
// ...
// Client init, ...
// ...
f, uploadedFile, _ := c.Request.FormFile("video")
if filepath.Ext(uploadedFile.Filename) != ".mp4" {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": "Uploaded file must be mp4 video",
})
return
}
// Upload file to Storage Bucket
sw := Services.StorageBucket.Object(fileUuid.String()).NewWriter(ctx)
_, err = io.Copy(sw, f)
err = sw.Close()
if err != nil {
return
}
Uploaded file is added as an record to the MongoDB database, so we can reference it later. We save video file name, size and unique UUID, which we can reference when retrieving the private object.
// ...
coll := Services.MongoClient.Database(os.Getenv("MONGO_DB_DATABASE")).Collection("videos")
doc := bson.D{{"filename", uploadedFile.Filename}, {"size", uploadedFile.Size}, {"uuid", fileUuid.String()}}
mongoRecord, err := coll.InsertOne(context.TODO(), doc)
if err != nil {
panic(err)
}
3.File can be sent to Google Video Intelligence API, and retrieve the video features.
// ...
op, err := client.AnnotateVideo(ctx, &videopb.AnnotateVideoRequest{
InputUri: "gs://" + os.Getenv("GOOGLE_CLOUD_STORAGE_BUCKET") + "/" + fileUuid.String(),
Features: []videopb.Feature{
videopb.Feature_LABEL_DETECTION,
},
})
if err != nil {
log.Fatalf("Failed to start annotation job: %v", err)
}
resp, err := op.Wait(ctx)
if err != nil {
log.Fatalf("Failed to annotate: %v", err)
}
result := resp.GetAnnotationResults()[0]
Retrieved features are saved to the corresponding video entity in MongoDB database, where Atlas search can index them.
// ...
filter := bson.D{{"_id", mongoRecord.InsertedID}}
update := bson.D{{"$set", bson.D{{"features", featuresList}}}}
_, err = coll.UpdateOne(context.TODO(), filter, update)
if err != nil {
return
}
After that, the video upload process is complete. If you would like to see the whole method, and not only the code snippets, you can access it on the GitHub: https://github.com/tavsec/devto-mongodb-hackathon-api/blob/main/Controllers/VideoController.go
Video search
If user wants to search all of the keywords, there is a VideoSearch method for that. The steps to utilise the Atlas Search engine is the following:
type SearchBody struct {
Keyword string `form:"keyword"`
Page int64 `form:"page"`
PerPage int64 `form:"perPage"`
}
type PaginatedResult struct {
Videos []Video
Pagination pag.PaginationData
}
func VideoSearch(c *gin.Context) {
var searchBody SearchBody
err := c.BindQuery(&searchBody)
if err != nil {
println(err.Error())
err = c.AbortWithError(http.StatusBadRequest, err)
return
}
collection := Services.MongoClient.Database(os.Getenv("MONGO_DB_DATABASE")).Collection("videos")
searchStage := bson.M{"$search": bson.D{{"index", os.Getenv("MONGO_DB_SEARCH_INDEX")}, {"text", bson.D{{"path", bson.D{{"wildcard", "*"}}}, {"query", searchBody.Keyword}}}}}
aggPaginatedData, err := pag.New(collection).Context(context.TODO()).Limit(searchBody.PerPage).Page(searchBody.Page).Aggregate(searchStage)
if err != nil {
panic(err)
}
storageClient, _ := storage.NewClient(context.Background())
opts := &storage.SignedURLOptions{
Scheme: storage.SigningSchemeV4,
Method: "GET",
Expires: time.Now().Add(15 * time.Minute),
}
var results []Video
for _, raw := range aggPaginatedData.Data {
var v *Video
if marshallErr := bson.Unmarshal(raw, &v); marshallErr == nil {
v.SignedURL, _ = storageClient.Bucket(os.Getenv("GOOGLE_CLOUD_STORAGE_BUCKET")).SignedURL(v.UUID, opts)
results = append(results, *v)
}
}
c.JSON(http.StatusOK, PaginatedResult{Videos: results, Pagination: aggPaginatedData.Pagination})
}
As you can see, Atlas Search provides simple interface to search. All we need is the following line:
searchStage := bson.M{"$search": bson.D{{"index", os.Getenv("MONGO_DB_SEARCH_INDEX")}, {"text", bson.D{{"path", bson.D{{"wildcard", "*"}}}, {"query", searchBody.Keyword}}}}}
I left the Atlas Search parameters as default, as I discovered that they work great for my usecase. They search across all of the saved video features:
After that, the video upload process is complete. If you would like to see the whole method, and not only the code snippets, you can access it on the GitHub: https://github.com/tavsec/devto-mongodb-hackathon-api/blob/main/Controllers/VideoController.go
Link to Source Code
API
tavsec / devto-mongodb-hackathon-api
API server for Dev.to MongoDB hackathon, using Google Cloud services and Atlas search
devto-mongodb-hackathon-api
API for my DevTo MongoDB hackathon project. https://dev.to/timotej_avsec/video-content-search-using-mongodb-atlas-search-and-google-machine-learning-1f32
Build
Firstly, add you application_default_credentials.json
to the source of the project. This will enable Google Cloud API.
Then, edit .env file
mv .env.example .env
Edit contents of the .env file.
Finally, you can run the application as you would any other Golang application:
go build -o devto-mongodb-hackathon github.com/tavsec/devto-mongodb-hackathon-api
You can also use Docker:
docker-compose up -d
Frontend
tavsec / devto-mongodb-hackathon-app
Frontend application for DevTo Mongodb hackathon
devto-mongodb-hackathon-app
Frontend application for my DevTo MongoDB hackathon project. https://dev.to/timotej_avsec/video-content-search-using-mongodb-atlas-search-and-google-machine-learning-1f32
Build Setup
# create and update .env file
$ cp .env.example .env
# install dependencies
$ npm install
# serve with hot reload at localhost:3000
$ npm run dev
# build for production and launch server
$ npm run build
$ npm run start
# generate static project
$ npm run generate
Permissive License
Both repositories are licensed as MIT.
Final thoughts
I really enjoyed building this application, as I discovered and learned a lot about MongoDB, as well as Google Cloud APIs. I was also fascinated about ease of use of Atlas Search, as it basically worked out-of-the-box (well, for my use-case, anyways). I will deffinietly use MongoDB for some of my personal projects in the future.
Top comments (0)