» Go: Building Full-Text Search API with ElasticSearch » 2. Index Documents » 2.3 Send Index Requests

Send Index Requests

Let's add the API for indexing books, then we can put in some test data for later use.

Create domain/gateway/book_manager.go:

/*
Package gateway contains all domain gateways.
*/
package gateway

import (
	"context"

	"literank.com/fulltext-books/domain/model"
)

// BookManager manages all books
type BookManager interface {
	IndexBook(ctx context.Context, b *model.Book) (string, error)
}

Again, we‘re using 4 Layers Architecture Pattern. It will pay off when your project grows bigger and bigger. Read more here.

Create infrastructure/search/es.go:

/*
Package search does all search engine related implementations.
*/
package search

import (
	"context"

	"github.com/elastic/go-elasticsearch/v8"

	"literank.com/fulltext-books/domain/model"
)

const INDEX_BOOK = "book_idx"

// ElasticSearchEngine runs all index/search operations
type ElasticSearchEngine struct {
	client   *elasticsearch.TypedClient
	pageSize int
}

// NewEngine constructs a new ElasticSearchEngine
func NewEngine(address string, pageSize int) (*ElasticSearchEngine, error) {
	cfg := elasticsearch.Config{
		Addresses: []string{address},
	}
	client, err := elasticsearch.NewTypedClient(cfg)
	if err != nil {
		return nil, err
	}
	// Create the index
	return &ElasticSearchEngine{client, pageSize}, nil
}

// IndexBook indexes a new book
func (s *ElasticSearchEngine) IndexBook(ctx context.Context, b *model.Book) (string, error) {
	resp, err := s.client.Index(INDEX_BOOK).
		Request(b).
		Do(ctx)
	if err != nil {
		return "", err
	}
	return resp.Id_, nil
}

We‘re using the fully-typed API client (elasticsearch.TypedClient) to index the document.

By default, Elasticsearch allows you to index documents into an index that doesn‘t exist yet. When you index a document into a non-existent index, Elasticsearch will dynamically create the index for you with default settings. This can be convenient for certain use cases where you don‘t want to manage index creation explicitly.

Install yaml dependencies:

 go get -u gopkg.in/yaml.v3

Create infrastructure/config/config.go:

/*
Package config provides config structures and parse funcs.
*/
package config

import (
	"fmt"
	"os"

	"gopkg.in/yaml.v3"
)

// Config is the global configuration.
type Config struct {
	App    ApplicationConfig `json:"app" yaml:"app"`
	Search SearchConfig      `json:"search" yaml:"search"`
}

// SearchConfig is the configuration of search engines.
type SearchConfig struct {
	Address string `json:"address" yaml:"address"`
}

// ApplicationConfig is the configuration of main app.
type ApplicationConfig struct {
	Port     int `json:"port" yaml:"port"`
	PageSize int `json:"page_size" yaml:"page_size"`
}

// Parse parses config file and returns a Config.
func Parse(filename string) (*Config, error) {
	buf, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	c := &Config{}
	err = yaml.Unmarshal(buf, c)
	if err != nil {
		return nil, fmt.Errorf("failed to parse file %s: %v", filename, err)
	}
	return c, nil
}

Create config.yml:

app:
  port: 8080
  page_size: 10
search:
  address: "http://localhost:9200"

Caution: Do not directly track your config.yml file with Git, as this could potentially leak sensitive data. If necessary, only track the template of the configuration file.
e.g.

app:
  port: 8080
search:
  address: ""

Create application/executor/book_operator.go:

/*
Package executor handles request-response style business logic.
*/
package executor

import (
	"context"

	"literank.com/fulltext-books/domain/gateway"
	"literank.com/fulltext-books/domain/model"
)

// BookOperator handles book input/output and proxies operations to the book manager.
type BookOperator struct {
	bookManager gateway.BookManager
}

// NewBookOperator constructs a new BookOperator
func NewBookOperator(b gateway.BookManager) *BookOperator {
	return &BookOperator{bookManager: b}
}

// CreateBook creates a new book
func (o *BookOperator) CreateBook(ctx context.Context, b *model.Book) (string, error) {
	return o.bookManager.IndexBook(ctx, b)
}

Create application/wire_helper.go:

/*
Package application provides all common structures and functions of the application layer.
*/
package application

import (
	"literank.com/fulltext-books/domain/gateway"
	"literank.com/fulltext-books/infrastructure/config"
	"literank.com/fulltext-books/infrastructure/search"
)

// WireHelper is the helper for dependency injection
type WireHelper struct {
	engine *search.ElasticSearchEngine
}

// NewWireHelper constructs a new WireHelper
func NewWireHelper(c *config.Config) (*WireHelper, error) {
	engine, err := search.NewEngine(c.Search.Address, c.App.PageSize)
	if err != nil {
		return nil, err
	}

	return &WireHelper{engine}, nil
}

// BookManager returns an instance of BookManager
func (w *WireHelper) BookManager() gateway.BookManager {
	return w.engine
}

Create adapter/router.go:

/*
Package adapter adapts to all kinds of framework or protocols.
*/
package adapter

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"

	"literank.com/fulltext-books/application"
	"literank.com/fulltext-books/application/executor"
	"literank.com/fulltext-books/domain/model"
)

// RestHandler handles all restful requests
type RestHandler struct {
	bookOperator *executor.BookOperator
}

func newRestHandler(wireHelper *application.WireHelper) *RestHandler {
	return &RestHandler{
		bookOperator: executor.NewBookOperator(wireHelper.BookManager()),
	}
}

// MakeRouter makes the main router
func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
	rest := newRestHandler(wireHelper)
	// Create a new Gin router
	r := gin.Default()

	r.POST("/books", rest.createBook)
	return r, nil
}

// Create a new book
func (r *RestHandler) createBook(c *gin.Context) {
	var reqBody model.Book
	if err := c.ShouldBindJSON(&reqBody); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	bookID, err := r.bookOperator.CreateBook(c, &reqBody)
	if err != nil {
		fmt.Printf("Failed to create: %v\n", err)
		c.JSON(http.StatusNotFound, gin.H{"error": "failed to create"})
		return
	}
	c.JSON(http.StatusCreated, gin.H{"id": bookID})
}

Replace main.go with the following code:

package main

import (
	"fmt"

	"literank.com/fulltext-books/adapter"
	"literank.com/fulltext-books/application"
	"literank.com/fulltext-books/infrastructure/config"
)

const configFileName = "config.yml"

func main() {
	// Read the config
	c, err := config.Parse(configFileName)
	if err != nil {
		panic(err)
	}

	// Prepare dependencies
	wireHelper, err := application.NewWireHelper(c)
	if err != nil {
		panic(err)
	}

	// Build main router
	r, err := adapter.MakeRouter(wireHelper)
	if err != nil {
		panic(err)
	}
	// Run the server on the specified port
	if err := r.Run(fmt.Sprintf(":%d", c.App.Port)); err != nil {
		panic(err)
	}
}

Run go mod tidy to tidy the go.mod file:

go mod tidy

Run the server again and try it with curl:

go run main.go

Example request:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Da Vinci Code","author":"Dan Brown","published_at":"2003-03-18","content":"In the Louvre, a curator is found dead. Next to his body, an enigmatic message. It is the beginning of a race to discover the truth about the Holy Grail."}' \
  http://localhost:8080/books

Example response:

{"id":"C1Ok9I4BexLIwExhUXKX"}

Put in more test data

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"Harry Potter and the Philosopher\u0027s Stone","author":"J.K. Rowling","published_at":"1997-06-26","content":"A young boy discovers he is a wizard and begins his education at Hogwarts School of Witchcraft and Wizardry, where he uncovers the mystery of the Philosopher‘s Stone."}' \
  http://localhost:8080/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"To Kill a Mockingbird","author":"Harper Lee","published_at":"1960-07-11","content":"Set in the American South during the Great Depression, the novel explores themes of racial injustice and moral growth through the eyes of young Scout Finch."}' \
  http://localhost:8080/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Lord of the Rings","author":"J.R.R. Tolkien","published_at":"1954-07-29","content":"A hobbit named Frodo Baggins embarks on a perilous journey to destroy a powerful ring and save Middle-earth from the Dark Lord Sauron."}' \
  http://localhost:8080/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Catcher in the Rye","author":"J.D. Salinger","published_at":"1951-07-16","content":"Holden Caulfield narrates his experiences in New York City after being expelled from prep school, grappling with themes of alienation, identity, and innocence."}' \
  http://localhost:8080/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Alchemist","author":"Paulo Coelho","published_at":"1988-01-01","content":"Santiago, a shepherd boy, travels from Spain to Egypt in search of a treasure buried near the Pyramids. Along the way, he learns about the importance of following one‘s dreams."}' \
  http://localhost:8080/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Hunger Games","author":"Suzanne Collins","published_at":"2008-09-14","content":"In a dystopian future, teenagers are forced to participate in a televised death match called the Hunger Games. Katniss Everdeen volunteers to take her sister‘s place and becomes a symbol of rebellion."}' \
  http://localhost:8080/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"1984","author":"George Orwell","published_at":"1949-06-08","content":"Winston Smith lives in a totalitarian society ruled by the Party led by Big Brother. He rebels against the oppressive regime but ultimately succumbs to its control."}' \
  http://localhost:8080/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Girl with the Dragon Tattoo","author":"Stieg Larsson","published_at":"2005-08-01","content":"Journalist Mikael Blomkvist and hacker Lisbeth Salander investigate the disappearance of a young woman from a wealthy family, uncovering dark secrets and corruption."}' \
  http://localhost:8080/books

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"Gone Girl","author":"Gillian Flynn","published_at":"2012-06-05","content":"On their fifth wedding anniversary, Nick Dunne‘s wife, Amy, disappears. As the media circus ensues and suspicions mount, Nick finds himself in a whirlwind of deception and betrayal."}' \
  http://localhost:8080/books