Unleashing CRUD Prowess: OpenSearch and Golang, a Dynamic Duo
Let's explore the powerful combination of OpenSearch, a distributed and open-source database for search and analytics, with Golang — a statically typed, and compiled programming language. This blog post delves into leveraging the strengths of both technologies to build efficient and scalable CRUD (Create, Read, Update, and Delete) applications.
go get github.com/opensearch-project/opensearch-go
go get github.com/aws/aws-sdk-go-v2/aws
go get github.com/aws/aws-sdk-go-v2/config
opensearch.NewClient()
function to create a new client.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const endpoint = "https://community-aws-os-with-golang.us-east-1.es.amazonaws.com"
func main() {
client, err := opensearch.NewClient(opensearch.Config{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
},
Addresses: []string{endpoint},
Username: "<YOUR_USERNAME>",
Password: "<YOUR_PASSWORD>",
})
if err != nil {
log.Fatal(err)
}
}
opensearch.Config
struct you can define this configuration. It supports many fields that you can use to access different OpenSearch deployments. The most important field is the Addresses
array, which expects you to inform one or multiple endpoints that can establish the connection. All the other fields are optional, and you may use them when it makes sense.Transport
. Similarly, it provides a username and a password using the fields Username
and Password
, to authenticate with the cluster exposed at the provided endpoint. This approach may work well if your cluster is running on-premises, or if your Amazon OpenSearch cluster is configured to use the master database for authentication.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func main() {
ctx := context.Background()
awsCfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion("<AWS_REGION>"),
config.WithCredentialsProvider(
getCredentialProvider(
"<AWS_ACCESS_KEY>",
"<AWS_SECRET_ACCESS_KEY>",
"<AWS_SESSION_TOKEN>"),
),
)
if err != nil {
log.Fatal(err)
}
signer, err := requestsigner.NewSignerWithService(awsCfg, "es")
if err != nil {
log.Fatal(err)
}
client, err := opensearch.NewClient(opensearch.Config{
Addresses: []string{endpoint},
Signer: signer,
})
if err != nil {
log.Fatal("client creation err", err)
}
}
func getCredentialProvider(accessKey, secretAccessKey, token string) aws.CredentialsProviderFunc {
return func(ctx context.Context) (aws.Credentials, error) {
c := &aws.Credentials{
AccessKeyID: accessKey,
SecretAccessKey: secretAccessKey,
SessionToken: token,
}
return *c, nil
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Before
client, err := opensearch.NewClient(opensearch.Config{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
},
Addresses: []string{endpoint},
Username: "<YOUR_USERNAME>",
Password: "<YOUR_PASSWORD>",
})
// After
client, err := opensearch.NewClient(opensearch.Config{
Addresses: []string{endpoint},
Signer: signer,
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pingRequest := opensearchapi.PingRequest{
Pretty: true,
Human: true,
ErrorTrace: true,
}
pingResponse, err := pingRequest.Do(ctx, client)
if err != nil {
log.Fatal(err)
} else {
if pingResponse.StatusCode == http.StatusOK {
log.Debug("🎵 Don't worry about a thing. " +
"'Cause every little thing gonna be alright.")
}
}
http.StatusOK
from Go to compare the status of the ping request. As mentioned earlier, OpenSearch makes all its operations available to the world via REST APIs. Therefore, if you need to check if the connection has been successfully established, you will check if the status is 200
. Similarly, if you need to check if the authentication failed, you will check if the status is equal to 401
or 403
, for example.1
2
3
4
5
6
{
"fullname": "Deadpool",
"identity": "Wade Wilson",
"knownas": "Merch with a mouth",
"type": "anti-hero"
}
1
2
3
4
5
6
7
type ComicCharacter struct {
ID string `json:"_id,omitempty"`
FullName string `json:"fullname,omitempty"`
Identity string `json:"identity,omitempty"`
KnownAs string `json:"knownas,omitempty"`
Type string `json:"type,omitempty"`
}
ID
in the struct. This will hold the document identifier, which is a string that uniquely identifies each document on OpenSearch. This is how you are going to index documents.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
comicCharacter := &ComicCharacter{
FullName: "Deadpool",
Identity: "Wade Wilson",
KnownAs: "Merch with a mouth",
Type: "anti-hero",
}
bodyContent, _ := json.Marshal(comicCharacter)
bodyReader := bytes.NewReader(bodyContent)
indexRequest := opensearchapi.IndexRequest{
Index: "build-on-aws",
Body: bodyReader,
}
indexResponse, err := indexRequest.Do(ctx, client)
if err != nil {
log.Error(err)
}
1
2
3
4
type BackendResponse struct {
ID string `json:"_id"`
Source *ComicCharacter `json:"_source"`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defer indexResponse.Body.Close()
bodyContent, err = io.ReadAll(indexResponse.Body)
if err != nil {
log.Error(err)
}
response := &BackendResponse{}
err = json.Unmarshal(bodyContent, response)
if err != nil {
log.Error(err)
}
documentID := response.ID
log.Info(documentID)
documentID
value retrieved earlier, here is how you can look up the document and write the value of the FullName
field to a variable called fullName
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
getRequest := opensearchapi.GetRequest{
Index: "build-on-aws",
DocumentID: documentID,
}
getResponse, err := getRequest.Do(ctx, client)
if err != nil {
log.Error(err)
}
defer getResponse.Body.Close()
bodyContent, err := io.ReadAll(getResponse.Body)
if err != nil {
log.Error(err)
}
response := &BackendResponse{}
err = json.Unmarshal(bodyContent, response)
if err != nil {
log.Error(err)
}
log.Info(response.Source.FullName)
KnownAs
field of the Deadpool character to Weapon XI
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
updatedDocument := &struct {
Doc ComicCharacter `json:"doc,omitempty"`
}{
Doc: ComicCharacter{
KnownAs: "Weapon XI",
},
}
bodyContent, err := json.Marshal(updatedDocument)
if err != nil {
log.Error(err)
}
bodyReader := bytes.NewReader(bodyContent)
updateRequest := opensearchapi.UpdateRequest{
Index: "build-on-aws",
DocumentID: documentID,
Body: bodyReader,
}
_, err = updateRequest.Do(ctx, client)
if err != nil {
log.Error(err)
}
1
2
3
4
5
6
7
8
9
10
deleteRequest := opensearchapi.DeleteRequest{
Index: backendIndex,
DocumentID: documentID,
}
_, err := deleteRequest.Do(ctx, client)
if err != nil {
log.Error(err)
}
Matt Murdock
. If anything is found, you will retrieve the first document returned and read the FullName
field into a variable called fullName
.Identity
. In Query DSL, this is how you would implement this query:1
2
3
4
5
6
7
8
GET _search
{
"query": {
"match": {
"identity": "Matt Murdock"
}
}
}
1
2
3
4
5
6
7
8
9
searchBody := &struct {
Query struct {
Match struct {
Identity string `json:"identity,omitempty"`
} `json:"match,omitempty"`
} `json:"query,omitempty"`
}{}
searchBody.Query.Match.Identity = "Matt Murdock"
1
2
3
4
5
6
7
8
9
10
11
12
13
bodyContent, _ := json.Marshal(searchBody)
bodyReader := bytes.NewReader(bodyContent)
searchRequest := opensearchapi.SearchRequest{
Index: []string{"build-on-aws"},
Body: bodyReader,
}
searchResponse, err := searchRequest.Do(ctx, client)
if err != nil {
log.Error(err)
}
1
2
3
4
5
6
7
8
9
10
11
type BackendSearchResponse struct {
Hits struct {
Total struct {
Value int64 `json:"value"`
} `json:"total"`
Hits []*struct {
ID string `json:"_id"`
Source *ComicCharacter `json:"_source"`
} `json:"hits"`
} `json:"hits"`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defer searchResponse.Body.Close()
bodyContent, err = io.ReadAll(searchResponse.Body)
if err != nil {
log.Error(err)
}
response := &BackendSearchResponse{}
err = json.Unmarshal(bodyContent, response)
if err != nil {
log.Error(err)
}
if response.Hits.Total.Value > 0 {
character := response.Hits.Hits[0].Source
log.Info(character.FullName)
} else {
log.Warn("No characters has been found.")
}
Matt Murdock
will return the character Daredevil
. Also, did you notice that it's the t-shirt I am wearing in the banner of this blog post? If caught that, add a comment below.SELECT * FROM build-on-aws WHERE identity IS 'Matt Murdock'
Matt Murdock
in the build-on-aws
index, which is the index where all the documents have been written. OpenSearch allows you to execute this SQL query using a plugin. Therefore, you need to write a code that will send an SQL query request to be executed in this plugin.io.Reader
just like you have done in the previous version of the search implementation.1
2
3
4
5
6
7
8
sqlQuery := &struct {
Query string `json:"query,omitempty"`
}{
Query: "SELECT * FROM build-on-aws WHERE identity IS 'Matt Murdock'",
}
bodyContent, _ := json.Marshal(sqlQuery)
bodyReader := bytes.NewReader(bodyContent)
1
2
3
4
5
6
7
8
9
10
11
12
sqlSearchRequest, err := http.NewRequest("POST", "_plugins/_sql?format=json", bodyReader)
sqlSearchRequest.Header.Add("Content-type", "application/json")
if err != nil {
log.Error(err)
}
sqlSearchResponse, err := client.Perform(sqlSearchRequest)
if err != nil {
log.Error(err)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defer sqlSearchResponse.Body.Close()
bodyContent, err = io.ReadAll(sqlSearchResponse.Body)
if err != nil {
log.Error(err)
}
response := &BackendSearchResponse{}
err = json.Unmarshal(bodyContent, response)
if err != nil {
log.Error(err)
}
if response.Hits.Total.Value > 0 {
character := response.Hits.Hits[0].Source
log.Info(character.FullName)
} else {
log.Warn("No characters has been found.")
}