본문 바로가기
프로그래밍/Go

[채팅-2] go elastic search client

by 동네로봇 2021. 4. 29.

개인 프로젝트로 go를 사용한 채팅 프로그램을 만들었다.

아래 블로그를 참고하였고, 필요한 부분은 수용하고 나와 맞지 않는 부분은 수정하여 프로젝트에 적용해보았다. 

 

thebook.io/006806/ch09/01/

 

Go 언어 웹 프로그래밍 철저 입문: 9.1 채팅 애플리케이션 만들기

 

thebook.io

 

프로젝트에서 사용한 주요 기술 및 언어

  • 서버 : golang
  • 회원 정보 관리 : mysql
  • 채팅방, 메세지 정보 관리 :  elastic search, kibana

서버에서 사용한 외부 오픈소스 

  • 라우터 : github.com/julienschmidt/httprouter
  • 미들웨어 : github.com/urfave/negroni
  • 뷰(렌더링) : github.com/unrolled/render
  • 세션 : github.com/goincremental/negroni-sessions
  • 웹소켓 : github.com/gorilla/websocket
  • 엘라스틱 서치 : github.com/olivere/elastic

 

이전 게시물

2021.04.28 - [프로그래밍/golang] - [채팅-1] go의 라우터, 미들웨어, 컨트롤러

 


먼저, 리눅스에다가 es 설치 할 시간이 없어서 그냥 window버전의 elastic search 를 로컬에 설치하였다. 

 

/****

참고로 window cmd 창에서 es서버에 curl로 쿼리를 날리면 실행문에 \n 이나 \t 같은게 제멋대로 붙어서 오류가 난다. (망할 윈도우)

쿼리가 길어지면 길어질수록 curl 로 쿼리를 날려서 테스트 하는 것이 거의 불가능.

그냥 kibana를 같이 설치해서 쿼리 테스트 하는게 스트레스 덜 받으니 키바나 설치 추천...^^...

****/

 

이제 go에서 elastic search 서버에 접근할 수 있는 클라이언트를 생성해보자.

 

github.com/elastic/go-elasticsearch

위의 링크가 공식적으로 제공하는 go-elasticsearch 패키지이지만, 사용법도 제대로 나와있지 않았고 json 쿼리를 요청하는 방법이 너무 복잡해서 사용하지 않았다.

 

 

 

github.com/olivere/elastic

 

olivere/elastic

Elasticsearch client for Go. Contribute to olivere/elastic development by creating an account on GitHub.

github.com

대신 위의 패키지를 사용하도록 한다. 쿼리 날리는 방법도 훨씬 간단하다. 

 

go get "github.com/olivere/elastic" 

작업 경로에서 go get 을 통해 사용할 패키지를 다운로드한다. 

 

현재 최신 버전이 v7.x 인데 이 버전을 사용하기 위해서는 go mod 를 설정해주어야 하고, 6.x 버전의 기능으로도 충분히 사용 가능했기 때문에 그냥 6.x 버전을 go get 으로 디펜던시 추가해주었다. 

 

채팅 프로그램의 채팅방과 메세지 정보를 저장하기 위해, es 에는 rooms와 messages 라는 index를 만들었다.

 

rooms 인덱스를 예로 들어 설명하겠다.

rooms 인덱스의 mapping 과 setting 데이터는 아래와 같이 설정해주었다.

PUT rooms
{
  "settings": {
    "number_of_shards": 1,
    "max_ngram_diff": "30",
    "analysis": {
      "analyzer": {
        "text_ngram_analyzer": {
          "tokenizer": "text_ngram_analyzer"
        }
      },
      "tokenizer": {
        "text_ngram_analyzer": {
          "type": "ngram",
          "min_gram": "1",
          "max_gram": "30",
          "token_chars": [
            "letter",
            "digit",
            "whitespace",
            "punctuation"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "create_date": {
        "type": "date",
        "format": "date_optional_time||yyyy/MM/dd"
      },
      "title": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          },
          "ngram": {
            "type": "text",
            "analyzer": "text_ngram_analyzer"
          }
        }
      },
      "users": {
        "type": "nested",
        "properties": {
          "id": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

 

rooms 에는 create_date, title, users 3개의 컬럼이 있다. id는 데이터 삽입 시 자동으로 채번된다.

go 서버에서 client를 생성하여 새로운 채팅방을 생성하는 쿼리를 날려주었다.

 

채팅방 생성

type Room struct {
	ID          string        `json:"id"`
	Title       string        `json:"title"`
	Create_date string        `json:"create_date"`
	Users       []interface{} `json:"users"`
}

func CreateRoom(w http.ResponseWriter, req *http.Request, p httprouter.Params) {

	// reqbody 에서 es query 에 필요한 데이터를 찾는다.
	reqBody, err := parser.HTMLRequestBodyParser(req)
	if nil != err {
		log.Fatal(err)
		return
	}

	title := reqBody["title"]
	userId := reqBody["userid"]
	id := map[string]string{
		"id": userId,
	}
	ids := make([]interface{}, 0)
	ids = append(ids, id)

	now := time.Now()
	strTime := now.Format(time.RFC3339)
	idx := strings.Index(strTime, "+")
	strTime = strTime[:idx]
	strTime = strTime + ".000Z"

	// req 에서 찾은 값으로 쿼리를 생성한다.
	var buf bytes.Buffer
	buf.WriteString(`{"create_date" : "`)
	buf.WriteString(strTime)
	buf.WriteString(`", "title" : "`)
	buf.WriteString(title)
	buf.WriteString(`", "users" : [{ "id" : "`)
	buf.WriteString(userId)
	buf.WriteString(`" }]}`)
	strQuery := buf.String()
	fmt.Println(strQuery)

	// 새로운 es client 를 생성한다.
	ctx := context.Background()

	esCli, err := elastic.NewClient()
	if nil != err {
		log.Fatal(err)
		return
	}

	// 쿼리를 요청하여 응답 값을 받는다.
	createRes, err := esCli.Index().
		Index("rooms").
		BodyString(strQuery).
		Do(ctx)
	if nil != err {
		log.Fatal(err)
		return
	}

	// 생성된 채팅방 정보를 구조체에 넣어준다.
	var room Room
	if strings.Compare(createRes.Result, "created") == 0 {
		room = Room{
			ID:          createRes.Id,
			Create_date: strTime,
			Title:       title,
			Users:       ids,
		}
	}
    // 응답 값을 flush 하여 메모리에 들고 있는 것을 날려준다. 
	_, err = esCli.Flush("rooms").Do(ctx)
	if nil != err {
		log.Fatal(err)
	}

	// 생성된 채팅방 구조체 데이터를 json으로 변환하여 현재 html 페이지로 전달한다.
	ren := r.GetInstance()
	ren.JSON(w, http.StatusOK, room)
}

 

채팅방 메인 페이지에서는 현재 생성되어 있는 채팅방을 보여주어야 하므로, 전체 채팅방의 데이터를 가져오는 쿼리를 생성해보자.

 

채팅방 검색

type Room struct {
	ID          string        `json:"id"`
	Title       string        `json:"title"`
	Create_date string        `json:"create_date"`
	Users       []interface{} `json:"users"`
}

func SearchRooms(w http.ResponseWriter, req *http.Request, p httprouter.Params) {

	// create new client
	ctx := context.Background()

	esCli, err := elastic.NewClient()
	if nil != err {
		log.Fatal(err)
		return
	}

	//build Query
	u := GetCurrentUser(req) // 세션에서 현재 로그인한 아이디 가져옴

	bq := elastic.NewBoolQuery()
	bq = bq.Must(elastic.NewTermQuery("users.id", u.ID))
	q := elastic.NewNestedQuery("users", bq)

	// send query
	searchRes, err := esCli.Search().
		Index("rooms").
		Query(q).
		Sort("create_date", true).
		From(0).Size(10).
		Pretty(true).
		Do(ctx)
	if nil != err {
		log.Fatal(err)
		return
	}

	fmt.Printf("Found total of %d rooms\n", searchRes.TotalHits())
    
	var arrRoom []Room
	if searchRes.TotalHits() > 0 {
		for _, hit := range searchRes.Hits.Hits {

			var r map[string]interface{}
			err := json.Unmarshal(hit.Source, &r)
			if nil != err {
				log.Fatal(err)
				return
			}

			newRoom := Room{
				ID:          hit.Id,
				Create_date: r["create_date"].(string),
				Title:       r["title"].(string),
				Users:       r["users"].([]interface{}),
			}

			// room 구조체를 담고 있는 slice에 검색한 결과를 하나씩 저장함.
			arrRoom = append(arrRoom, newRoom)
		}
	}
	_, err = esCli.Flush("rooms").Do(ctx)
	if nil != err {
		log.Fatal(err)
	}

	// slice 데이터를 json으로 변환하여 html 에 전달한다.
	ren := r.GetInstance()
	ren.JSON(w, http.StatusOK, arrRoom)
}

 

해당 패키지 함수를 사용하여, 대부분의 es 쿼리를 요청할 수 있다. 

패키지에서 쿼리를 어떻게 생성하여 request 해야하는지를 더 자세히 참고하려면 아래 github 에 잘 정리되어 있다.

olivere.github.io/elastic/

 

Elastic: An Elasticsearch client for Go

Introduction Elastic is a client for Elasticsearch for the Go programming language. We use it in production since 2012. It supports Elasticsearch versions 1.x, 2.x, 5.x, 6.x and 7.x. The code is MIT licensed and hosted on GitHub. Please report issues on Gi

olivere.github.io