这是一个用Golang编写的简单Elasticsearch CRUD例子。请记住,有些文件需要改进。有一些硬编码的部分,重复的部分等等。我必须让这个帖子尽可能的简短。请随意重构它。
我们的例子使用了索引别名,这比具体的索引要好。另外,我们使用UUID作为文档的ID,而不是依赖Elasticsearch的自定义ID,_id 。如果你不想走这条路,那也完全没问题。
运行make docker-up 命令后,在浏览器中访问http://localhost:5000 (ElasticHQ)或http://localhost:9000 (Cerebro),并连接到http://docker_elasticsearch_1:9200 ,以获得UIs。你可以使用你喜欢的任何一种。
结构
├── Makefile
├── cmd
│ └── elastic
│ └── main.go
├── docker
│ ├── docker-compose.yaml
│ └── tmp
│ └── elasticsearch
└── internal
├── pkg
│ ├── domain
│ │ └── error.go
│ └── storage
│ ├── elasticsearch
│ │ ├── elasticsearch.go
│ │ └── post_storage.go
│ └── post_storer.go
└── post
├── handler.go
├── request.go
├── response.go
└── service.go
文件
制作文件
.PHONY: docker-up
docker-up:
docker-compose -f docker/docker-compose.yaml up
.PHONY: docker-down
docker-down:
docker-compose -f docker/docker-compose.yaml down
docker system prune --volumes --force
.PHONY: up
up:
go run -race cmd/elastic/main.go
docker-compose.yaml
version: "3.7"
services:
elasticsearch:
image: "docker.elastic.co/elasticsearch/elasticsearch:7.12.1"
environment:
discovery.type: "single-node"
cluster.name: "cluster-1"
node.name: "node-1"
ports:
- "9200:9200"
volumes:
- "./tmp/elasticsearch/data:/usr/share/elasticsearch/data"
elastichq:
image: "elastichq/elasticsearch-hq"
ports:
- "5000:5000"
depends_on:
- "elasticsearch"
cerebro:
image: "lmenezes/cerebro"
ports:
- "9000:9000"
depends_on:
- "elasticsearch"
main.go
package main
import (
"log"
"net/http"
"github.com/you/elastic/internal/pkg/storage/elasticsearch"
"github.com/you/elastic/internal/post"
"github.com/julienschmidt/httprouter"
)
func main() {
// Bootstrap elasticsearch.
elastic, err := elasticsearch.New([]string{"http://0.0.0.0:9200"})
if err != nil {
log.Fatalln(err)
}
if err := elastic.CreateIndex("post"); err != nil {
log.Fatalln(err)
}
// Bootstrap storage.
storage, err := elasticsearch.NewPostStorage(*elastic)
if err != nil {
log.Fatalln(err)
}
// Bootstrap API.
postAPI := post.New(storage)
// Bootstrap HTTP router.
router := httprouter.New()
router.HandlerFunc("POST", "/api/v1/posts", postAPI.Create)
router.HandlerFunc("PATCH", "/api/v1/posts/:id", postAPI.Update)
router.HandlerFunc("DELETE", "/api/v1/posts/:id", postAPI.Delete)
router.HandlerFunc("GET", "/api/v1/posts/:id", postAPI.Find)
// Start HTTP server.
log.Fatalln(http.ListenAndServe(":3000", router))
}
error.go
package domain
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
)
elasticsearch.go
package elasticsearch
import (
"fmt"
"github.com/elastic/go-elasticsearch/v7"
)
type ElasticSearch struct {
client *elasticsearch.Client
index string
alias string
}
func New(addresses []string) (*ElasticSearch, error) {
cfg := elasticsearch.Config{
Addresses: addresses,
}
client, err := elasticsearch.NewClient(cfg)
if err != nil {
return nil, err
}
return &ElasticSearch{
client: client,
}, nil
}
func (e *ElasticSearch) CreateIndex(index string) error {
e.index = index
e.alias = index + "_alias"
res, err := e.client.Indices.Exists([]string{e.index})
if err != nil {
return fmt.Errorf("cannot check index existence: %w", err)
}
if res.StatusCode == 200 {
return nil
}
if res.StatusCode != 404 {
return fmt.Errorf("error in index existence response: %s", res.String())
}
res, err = e.client.Indices.Create(e.index)
if err != nil {
return fmt.Errorf("cannot create index: %w", err)
}
if res.IsError() {
return fmt.Errorf("error in index creation response: %s", res.String())
}
res, err = e.client.Indices.PutAlias([]string{e.index}, e.alias)
if err != nil {
return fmt.Errorf("cannot create index alias: %w", err)
}
if res.IsError() {
return fmt.Errorf("error in index alias creation response: %s", res.String())
}
return nil
}
// document represents a single document in Get API response body.
type document struct {
Source interface{} `json:"_source"`
}
post_storage.go
package elasticsearch
import (
"bytes"
"context"
"encoding/json"
"fmt"
"time"
"github.com/you/elastic/internal/pkg/domain"
"github.com/you/elastic/internal/pkg/storage"
"github.com/elastic/go-elasticsearch/v7/esapi"
)
var _ storage.PostStorer = PostStorage{}
type PostStorage struct {
elastic ElasticSearch
timeout time.Duration
}
func NewPostStorage(elastic ElasticSearch) (PostStorage, error) {
return PostStorage{
elastic: elastic,
timeout: time.Second * 10,
}, nil
}
func (p PostStorage) Insert(ctx context.Context, post storage.Post) error {
bdy, err := json.Marshal(post)
if err != nil {
return fmt.Errorf("insert: marshall: %w", err)
}
// res, err := p.elastic.client.Create()
req := esapi.CreateRequest{
Index: p.elastic.alias,
DocumentID: post.ID,
Body: bytes.NewReader(bdy),
}
ctx, cancel := context.WithTimeout(ctx, p.timeout)
defer cancel()
res, err := req.Do(ctx, p.elastic.client)
if err != nil {
return fmt.Errorf("insert: request: %w", err)
}
defer res.Body.Close()
if res.StatusCode == 409 {
return domain.ErrConflict
}
if res.IsError() {
return fmt.Errorf("insert: response: %s", res.String())
}
return nil
}
func (p PostStorage) Update(ctx context.Context, post storage.Post) error {
bdy, err := json.Marshal(post)
if err != nil {
return fmt.Errorf("update: marshall: %w", err)
}
// res, err := p.elastic.client.Update()
req := esapi.UpdateRequest{
Index: p.elastic.alias,
DocumentID: post.ID,
Body: bytes.NewReader([]byte(fmt.Sprintf(`{"doc":%s}`, bdy))),
}
ctx, cancel := context.WithTimeout(ctx, p.timeout)
defer cancel()
res, err := req.Do(ctx, p.elastic.client)
if err != nil {
return fmt.Errorf("update: request: %w", err)
}
defer res.Body.Close()
if res.StatusCode == 404 {
return domain.ErrNotFound
}
if res.IsError() {
return fmt.Errorf("update: response: %s", res.String())
}
return nil
}
func (p PostStorage) Delete(ctx context.Context, id string) error {
// res, err := p.elastic.client.Delete()
req := esapi.DeleteRequest{
Index: p.elastic.alias,
DocumentID: id,
}
ctx, cancel := context.WithTimeout(ctx, p.timeout)
defer cancel()
res, err := req.Do(ctx, p.elastic.client)
if err != nil {
return fmt.Errorf("delete: request: %w", err)
}
defer res.Body.Close()
if res.StatusCode == 404 {
return domain.ErrNotFound
}
if res.IsError() {
return fmt.Errorf("delete: response: %s", res.String())
}
return nil
}
func (p PostStorage) FindOne(ctx context.Context, id string) (storage.Post, error) {
// res, err := p.elastic.client.Get()
req := esapi.GetRequest{
Index: p.elastic.alias,
DocumentID: id,
}
ctx, cancel := context.WithTimeout(ctx, p.timeout)
defer cancel()
res, err := req.Do(ctx, p.elastic.client)
if err != nil {
return storage.Post{}, fmt.Errorf("find one: request: %w", err)
}
defer res.Body.Close()
if res.StatusCode == 404 {
return storage.Post{}, domain.ErrNotFound
}
if res.IsError() {
return storage.Post{}, fmt.Errorf("find one: response: %s", res.String())
}
var (
post storage.Post
body document
)
body.Source = &post
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
return storage.Post{}, fmt.Errorf("find one: decode: %w", err)
}
return post, nil
}
post_storer.go
package storage
import (
"context"
"time"
)
type PostStorer interface {
Insert(ctx context.Context, post Post) error
Update(ctx context.Context, post Post) error
Delete(ctx context.Context, id string) error
FindOne(ctx context.Context, id string) (Post, error)
}
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Text string `json:"text"`
Tags []string `json:"tags"`
CreatedAt *time.Time `json:"created_at,omitempty"`
}
handler.go
package post
import (
"encoding/json"
"log"
"net/http"
"github.com/you/elastic/internal/pkg/domain"
"github.com/you/elastic/internal/pkg/storage"
"github.com/julienschmidt/httprouter"
)
type Handler struct {
service service
}
func New(storage storage.PostStorer) Handler {
return Handler{
service: service{storage: storage},
}
}
// POST /api/v1/posts
func (h Handler) Create(w http.ResponseWriter, r *http.Request) {
var req createRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println(err)
return
}
res, err := h.service.create(r.Context(), req)
if err != nil {
switch err {
case domain.ErrConflict:
w.WriteHeader(http.StatusConflict)
default:
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
}
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
bdy, _ := json.Marshal(res)
_, _ = w.Write(bdy)
}
// PATCH /api/v1/posts/:id
func (h Handler) Update(w http.ResponseWriter, r *http.Request) {
var req updateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println(err)
return
}
req.ID = httprouter.ParamsFromContext(r.Context()).ByName("id")
if err := h.service.update(r.Context(), req); err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
}
return
}
w.WriteHeader(http.StatusNoContent)
}
// DELETE /api/v1/posts/:id
func (h Handler) Delete(w http.ResponseWriter, r *http.Request) {
id := httprouter.ParamsFromContext(r.Context()).ByName("id")
if err := h.service.delete(r.Context(), deleteRequest{ID: id}); err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
}
return
}
w.WriteHeader(http.StatusNoContent)
}
// GET /api/v1/posts/:id
func (h Handler) Find(w http.ResponseWriter, r *http.Request) {
id := httprouter.ParamsFromContext(r.Context()).ByName("id")
res, err := h.service.find(r.Context(), findRequest{ID: id})
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
}
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
bdy, _ := json.Marshal(res)
_, _ = w.Write(bdy)
}
请求.go
package post
type createRequest struct {
Title string `json:"title"`
Text string `json:"text"`
Tags []string `json:"tags"`
}
type updateRequest struct {
ID string
Title string `json:"title"`
Text string `json:"text"`
Tags []string `json:"tags"`
}
type deleteRequest struct {
ID string
}
type findRequest struct {
ID string
}
响应.go
package post
import "time"
type createResponse struct {
ID string `json:"id"`
}
type findResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Text string `json:"text"`
Tags []string `json:"tags"`
CreatedAt time.Time `json:"created_at"`
}
service.go
package post
import (
"context"
"time"
"github.com/you/elastic/internal/pkg/storage"
"github.com/google/uuid"
)
type service struct {
storage storage.PostStorer
}
func (s service) create(ctx context.Context, req createRequest) (createResponse, error) {
id := uuid.New().String()
cr := time.Now().UTC()
doc := storage.Post{
ID: id,
Title: req.Title,
Text: req.Text,
Tags: req.Tags,
CreatedAt: &cr,
}
if err := s.storage.Insert(ctx, doc); err != nil {
return createResponse{}, err
}
return createResponse{ID: id}, nil
}
func (s service) update(ctx context.Context, req updateRequest) error {
doc := storage.Post{
ID: req.ID,
Title: req.Title,
Text: req.Text,
Tags: req.Tags,
}
if err := s.storage.Update(ctx, doc); err != nil {
return err
}
return nil
}
func (s service) delete(ctx context.Context, req deleteRequest) error {
if err := s.storage.Delete(ctx, req.ID); err != nil {
return err
}
return nil
}
func (s service) find(ctx context.Context, req findRequest) (findResponse, error) {
post, err := s.storage.FindOne(ctx, req.ID)
if err != nil {
return findResponse{}, err
}
return findResponse{
ID: post.ID,
Title: post.Title,
Text: post.Text,
Tags: post.Tags,
CreatedAt: *post.CreatedAt,
}, nil
}
测试
curl --request POST 'http://localhost:3000/api/v1/posts' \
--data-raw '{
"title": "test title",
"text": "test text",
"tags": ["tag"]
}'
curl --request PATCH 'http://localhost:3000/api/v1/posts/{id}' \
--data-raw '{
"title": "test title x",
"text": "test text x",
"tags": ["tag x"]
}'
curl --request DELETE 'http://localhost:3000/api/v1/posts/{id}'
curl --request GET 'http://localhost:3000/api/v1/posts/{id}'