├── LICENSE ├── README.md ├── client.go ├── client_test.go ├── go.mod ├── helper_test.go └── structs.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 maxime lamure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elasticsearch with go (golang) 2 | 3 | A minimalistic elasticsearch client in Go (golang). 4 | 5 | This package is a basic wrapper which exposes Elasticsearch methods in GO. It helps you to stay focus on your queries and your index mapping by doing for you the communication with elasticsearch. 6 | 7 | 8 | ## Methods 9 | 10 | Index management: 11 | 12 | * CreateIndex 13 | * DeleteIndex 14 | * UpdateIndexSetting 15 | * IndexSettings 16 | * IndexExists 17 | * Status 18 | * GetIndicesFromAlias 19 | * UpdateAlias 20 | 21 | CRUD: 22 | 23 | * InsertDocument 24 | * Document 25 | * DeleteDocument 26 | 27 | Process: 28 | 29 | * Bulk 30 | * UpdateByQuery 31 | 32 | 33 | Queries: 34 | 35 | * Search 36 | * Multi Search 37 | * Suggest 38 | 39 | ## Compatibility 40 | 41 | Support all Elasticsearch versions 42 | 43 | 44 | ## Install 45 | 46 | go get github.com/maximelamure/elasticsearch 47 | 48 | 49 | ## Usage 50 | 51 | Below is an example which shows some common use cases. Check [client_test.go](https://github.com/maximelamure/elasticsearch/blob/master/client_test.go) for more usage. 52 | 53 | TBD 54 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | ) 14 | 15 | // Searcher set the contract to manage indices, synchronize data and request 16 | type Client interface { 17 | CreateIndex(indexName, mapping string) (*Response, error) 18 | DeleteIndex(indexName string) (*Response, error) 19 | UpdateIndexSetting(indexName, mapping string) (*Response, error) 20 | IndexSettings(indexName string) (Settings, error) 21 | IndexExists(indexName string) (bool, error) 22 | Status(indices string) (*Settings, error) 23 | InsertDocument(indexName, documentType, identifier string, data []byte) (*InsertDocument, error) 24 | Document(indexName, documentType, identifier string) (*Document, error) 25 | DeleteDocument(indexName, documentType, identifier string) (*Document, error) 26 | Bulk(indexName string, data []byte) (*Bulk, error) 27 | Search(indexName, documentType, data string, explain bool) (*SearchResult, error) 28 | MSearch(queries []MSearchQuery) (*MSearchResult, error) 29 | Suggest(indexName, data string) ([]byte, error) 30 | GetIndicesFromAlias(alias string) ([]string, error) 31 | UpdateAlias(remove []string, add []string, alias string) (*Response, error) 32 | UpdateByQuery(indexName, query string) (*UpdateByQueryResult, error) 33 | } 34 | 35 | // A SearchClient describes the client configuration to manage an ElasticSearch index. 36 | type client struct { 37 | Host url.URL 38 | } 39 | 40 | // NewSearchClient creates and initializes a new ElasticSearch client, implements core api for Indexing and searching. 41 | func NewClient(scheme, host, port string) Client { 42 | u := url.URL{ 43 | Scheme: scheme, 44 | Host: host + ":" + port, 45 | } 46 | return &client{Host: u} 47 | } 48 | 49 | // NewSearchClient creates and initializes a new ElasticSearch client, implements core api for Indexing and searching. 50 | func NewClientFromUrl(rawurl string) Client { 51 | u, err := url.Parse(rawurl) 52 | if err != nil { 53 | log.Fatal(err) 54 | return nil 55 | } 56 | return &client{Host: *u} 57 | } 58 | 59 | // CreateIndex instantiates an index 60 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html 61 | func (c *client) CreateIndex(indexName, mapping string) (*Response, error) { 62 | url := c.Host.String() + "/" + indexName 63 | reader := bytes.NewBufferString(mapping) 64 | response, err := sendHTTPRequest("PUT", url, reader) 65 | if err != nil { 66 | return &Response{}, err 67 | } 68 | 69 | esResp := &Response{} 70 | err = json.Unmarshal(response, esResp) 71 | if err != nil { 72 | return &Response{}, err 73 | } 74 | 75 | return esResp, nil 76 | } 77 | 78 | // DeleteIndex deletes an existing index. 79 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html 80 | func (c *client) DeleteIndex(indexName string) (*Response, error) { 81 | url := c.Host.String() + "/" + indexName 82 | response, err := sendHTTPRequest("DELETE", url, nil) 83 | if err != nil { 84 | return &Response{}, err 85 | } 86 | 87 | esResp := &Response{} 88 | err = json.Unmarshal(response, esResp) 89 | if err != nil { 90 | return &Response{}, err 91 | } 92 | 93 | return esResp, nil 94 | } 95 | 96 | // UpdateIndexSetting changes specific index level settings in real time 97 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html 98 | func (c *client) UpdateIndexSetting(indexName, mapping string) (*Response, error) { 99 | url := c.Host.String() + "/" + indexName + "/_settings" 100 | reader := bytes.NewBufferString(mapping) 101 | response, err := sendHTTPRequest("PUT", url, reader) 102 | if err != nil { 103 | return &Response{}, err 104 | } 105 | 106 | esResp := &Response{} 107 | err = json.Unmarshal(response, esResp) 108 | if err != nil { 109 | return &Response{}, err 110 | } 111 | 112 | return esResp, nil 113 | } 114 | 115 | // IndexSettings allows to retrieve settings of index 116 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-settings.html 117 | func (c *client) IndexSettings(indexName string) (Settings, error) { 118 | url := c.Host.String() + "/" + indexName + "/_settings" 119 | response, err := sendHTTPRequest("GET", url, nil) 120 | if err != nil { 121 | return Settings{}, err 122 | } 123 | 124 | type settingsArray map[string]Settings 125 | dec := json.NewDecoder(bytes.NewBuffer(response)) 126 | var info settingsArray 127 | err = dec.Decode(&info) 128 | if err != nil { 129 | return Settings{}, err 130 | } 131 | 132 | return info[indexName], nil 133 | } 134 | 135 | // IndexExists allows to check if the index exists or not. 136 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-exists.html 137 | func (c *client) IndexExists(indexName string) (bool, error) { 138 | url := c.Host.String() + "/" + indexName 139 | httpClient := &http.Client{} 140 | newReq, err := httpClient.Head(url) 141 | if err != nil { 142 | return false, err 143 | } 144 | 145 | return newReq.StatusCode == http.StatusOK, nil 146 | } 147 | 148 | // Status allows to get a comprehensive status information 149 | func (c *client) Status(indices string) (*Settings, error) { 150 | url := c.Host.String() + "/" + indices + "/_status" 151 | response, err := sendHTTPRequest("GET", url, nil) 152 | if err != nil { 153 | return &Settings{}, err 154 | } 155 | 156 | esResp := &Settings{} 157 | err = json.Unmarshal(response, esResp) 158 | if err != nil { 159 | return &Settings{}, err 160 | } 161 | 162 | return esResp, nil 163 | } 164 | 165 | // InsertDocument adds or updates a typed JSON document in a specific index, making it searchable 166 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html 167 | func (c *client) InsertDocument(indexName, documentType, identifier string, data []byte) (*InsertDocument, error) { 168 | url := c.Host.String() + "/" + indexName + "/_doc/" + identifier 169 | reader := bytes.NewBuffer(data) 170 | response, err := sendHTTPRequest("POST", url, reader) 171 | if err != nil { 172 | return &InsertDocument{}, err 173 | } 174 | 175 | esResp := &InsertDocument{} 176 | err = json.Unmarshal(response, esResp) 177 | if err != nil { 178 | return &InsertDocument{}, err 179 | } 180 | 181 | return esResp, nil 182 | } 183 | 184 | // Document gets a typed JSON document from the index based on its id 185 | // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html 186 | func (c *client) Document(indexName, documentType, identifier string) (*Document, error) { 187 | url := c.Host.String() + "/" + indexName + "/" + documentType + "/" + identifier 188 | response, err := sendHTTPRequest("GET", url, nil) 189 | if err != nil { 190 | return &Document{}, err 191 | } 192 | 193 | esResp := &Document{} 194 | err = json.Unmarshal(response, esResp) 195 | if err != nil { 196 | return &Document{}, err 197 | } 198 | 199 | return esResp, nil 200 | } 201 | 202 | // DeleteDocument deletes a typed JSON document from a specific index based on its id 203 | // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html 204 | func (c *client) DeleteDocument(indexName, documentType, identifier string) (*Document, error) { 205 | url := c.Host.String() + "/" + indexName + "/" + documentType + "/" + identifier 206 | response, err := sendHTTPRequest("DELETE", url, nil) 207 | if err != nil { 208 | return &Document{}, err 209 | } 210 | 211 | esResp := &Document{} 212 | err = json.Unmarshal(response, esResp) 213 | if err != nil { 214 | return &Document{}, err 215 | } 216 | 217 | return esResp, nil 218 | } 219 | 220 | // Bulk makes it possible to perform many index/delete operations in a single API call. 221 | // This can greatly increase the indexing speed. 222 | // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html 223 | func (c *client) Bulk(indexName string, data []byte) (*Bulk, error) { 224 | url := c.Host.String() + "/" + indexName + "/_bulk" 225 | reader := bytes.NewBuffer(data) 226 | response, err := sendHTTPRequest("POST", url, reader) 227 | if err != nil { 228 | return &Bulk{}, err 229 | } 230 | 231 | esResp := &Bulk{} 232 | err = json.Unmarshal(response, esResp) 233 | if err != nil { 234 | return &Bulk{}, err 235 | } 236 | 237 | return esResp, nil 238 | } 239 | 240 | // Search allows to execute a search query and get back search hits that match the query 241 | // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html 242 | func (c *client) Search(indexName, documentType, data string, explain bool) (*SearchResult, error) { 243 | 244 | url := c.Host.String() + "/" + indexName + "/_search" 245 | if explain { 246 | url += "?explain" 247 | } 248 | reader := bytes.NewBufferString(data) 249 | response, err := sendHTTPRequest("POST", url, reader) 250 | if err != nil { 251 | return &SearchResult{}, err 252 | } 253 | 254 | esResp := &SearchResult{} 255 | err = json.Unmarshal(response, esResp) 256 | if err != nil { 257 | return &SearchResult{}, err 258 | } 259 | 260 | return esResp, nil 261 | } 262 | 263 | // MSearch allows to execute a multi-search and get back result 264 | // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-multi-search.html 265 | func (c *client) MSearch(queries []MSearchQuery) (*MSearchResult, error) { 266 | replacer := strings.NewReplacer("\n", " ") 267 | queriesList := make([]string, len(queries)) 268 | for i, query := range queries { 269 | queriesList[i] = query.Header + "\n" + replacer.Replace(query.Body) 270 | } 271 | 272 | mSearchQuery := strings.Join(queriesList, "\n") + "\n" // Don't forget trailing \n 273 | url := c.Host.String() + "/_msearch" 274 | reader := bytes.NewBufferString(mSearchQuery) 275 | response, err := sendHTTPRequest("POST", url, reader) 276 | 277 | if err != nil { 278 | return &MSearchResult{}, err 279 | } 280 | 281 | esResp := &MSearchResult{} 282 | err = json.Unmarshal(response, esResp) 283 | if err != nil { 284 | return &MSearchResult{}, err 285 | } 286 | 287 | return esResp, nil 288 | } 289 | 290 | // Suggest allows basic auto-complete functionality. 291 | // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters-completion.html 292 | func (c *client) Suggest(indexName, data string) ([]byte, error) { 293 | url := c.Host.String() + "/" + indexName + "/_suggest" 294 | reader := bytes.NewBufferString(data) 295 | response, err := sendHTTPRequest("POST", url, reader) 296 | return response, err 297 | } 298 | 299 | // GetIndicesFromAlias returns the list of indices the alias points to 300 | func (c *client) GetIndicesFromAlias(alias string) ([]string, error) { 301 | url := c.Host.String() + "/*/_alias/" + alias 302 | response, err := sendHTTPRequest("GET", url, nil) 303 | if err != nil { 304 | return []string{}, err 305 | } 306 | 307 | esResp := make(map[string]*json.RawMessage) 308 | err = json.Unmarshal(response, &esResp) 309 | if err != nil { 310 | return []string{}, err 311 | } 312 | 313 | indices := make([]string, len(esResp)) 314 | i := 0 315 | for k := range esResp { 316 | indices[i] = k 317 | i++ 318 | } 319 | return indices, nil 320 | } 321 | 322 | // UpdateAlias updates the indices on which the alias point to. 323 | // The change is atomic. 324 | func (c *client) UpdateAlias(remove []string, add []string, alias string) (*Response, error) { 325 | url := c.Host.String() + "/_aliases" 326 | body := getAliasQuery(remove, add, alias) 327 | reader := bytes.NewBufferString(body) 328 | 329 | response, err := sendHTTPRequest("POST", url, reader) 330 | if err != nil { 331 | return &Response{}, err 332 | } 333 | 334 | esResp := &Response{} 335 | err = json.Unmarshal(response, esResp) 336 | if err != nil { 337 | return &Response{}, err 338 | } 339 | 340 | return esResp, nil 341 | } 342 | 343 | // UpdateByQuery updates documents that match the specified query. 344 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html 345 | func (c *client) UpdateByQuery(indexName, query string) (*UpdateByQueryResult, error) { 346 | url := c.Host.String() + "/" + indexName + "/_update_by_query" 347 | reader := bytes.NewBufferString(query) 348 | response, err := sendHTTPRequest("POST", url, reader) 349 | if err != nil { 350 | return &UpdateByQueryResult{}, err 351 | } 352 | 353 | esResp := &UpdateByQueryResult{} 354 | err = json.Unmarshal(response, esResp) 355 | if err != nil { 356 | return &UpdateByQueryResult{}, err 357 | } 358 | 359 | return esResp, nil 360 | 361 | } 362 | 363 | func getAliasQuery(remove []string, add []string, alias string) string { 364 | actions := make([]string, len(remove)+len(add)) 365 | 366 | i := 0 367 | for _, index := range remove { 368 | actions[i] = "{ \"remove\": { \"index\": \"" + index + "\", \"alias\": \"" + alias + "\" }}" 369 | i++ 370 | } 371 | 372 | for _, index := range add { 373 | actions[i] = "{ \"add\": { \"index\": \"" + index + "\", \"alias\": \"" + alias + "\" }}" 374 | i++ 375 | } 376 | 377 | return "{\"actions\": [ " + strings.Join(actions, ",") + " ]}" 378 | } 379 | 380 | func sendHTTPRequest(method, url string, body io.Reader) ([]byte, error) { 381 | client := &http.Client{} 382 | req, err := http.NewRequest(method, url, body) 383 | if err != nil { 384 | return nil, err 385 | } 386 | 387 | // if method == "POST" || method == "PUT" { 388 | // req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 389 | // } 390 | 391 | req.Header.Set("Content-Type", "application/json") 392 | 393 | newReq, err := client.Do(req) 394 | if err != nil { 395 | return nil, err 396 | } 397 | 398 | defer newReq.Body.Close() 399 | response, err := ioutil.ReadAll(newReq.Body) 400 | if err != nil { 401 | return nil, err 402 | } 403 | 404 | if newReq.StatusCode > http.StatusCreated && newReq.StatusCode < http.StatusNotFound { 405 | return nil, errors.New(string(response)) 406 | } 407 | 408 | return response, nil 409 | } 410 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package elasticsearch_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | "time" 8 | 9 | "github.com/maximelamure/elasticsearch" 10 | ) 11 | 12 | var ( 13 | ProductDocumentType = "PRODUCT" 14 | ESScheme = "http" 15 | ESHost = "localhost" 16 | ESPort = "9200" 17 | ESSearchIndexName = "search" 18 | ESRecommendationIndexName = "recommendation" 19 | IndexName = "test" 20 | IndexMapping = `{"settings": 21 | { 22 | "number_of_shards" : 5, 23 | "number_of_replicas" : 1 24 | } 25 | }` 26 | SuggestionIndexName = "test" 27 | SuggestionIndexMapping = `{ 28 | "mappings": { 29 | "suggestion" : { 30 | "properties" : { 31 | "name_suggest" : { 32 | "type" : "completion", 33 | "payloads" : true 34 | } 35 | } 36 | } 37 | } 38 | }` 39 | ) 40 | 41 | func TestIndexManagement(t *testing.T) { 42 | helper := Test{} 43 | client := elasticsearch.NewClient(ESScheme, ESHost, ESPort) 44 | 45 | //If the index exists, remove it 46 | if response, _ := client.IndexExists(IndexName); response { 47 | delReponse, err := client.DeleteIndex(IndexName) 48 | helper.OK(t, err) 49 | helper.Assert(t, delReponse.Acknowledged, "Unable to remove existing index:"+delReponse.Error) 50 | } 51 | 52 | //Check if we have the test index 53 | response, err := client.IndexExists(IndexName) 54 | helper.OK(t, err) 55 | helper.Assert(t, !response, "Index has not been removed with the DeleteIndex function") 56 | 57 | //Create the index 58 | createResponse, err := client.CreateIndex(IndexName, IndexMapping) 59 | helper.OK(t, err) 60 | helper.Assert(t, createResponse.Acknowledged, "Index has not been created") 61 | 62 | //Check if the index has been created 63 | response, err = client.IndexExists(IndexName) 64 | helper.OK(t, err) 65 | helper.Assert(t, response, "Index has not been created with the CreateIndex function") 66 | 67 | //Delete the index 68 | deleteResponse, err := client.DeleteIndex(IndexName) 69 | helper.OK(t, err) 70 | helper.Assert(t, deleteResponse.Acknowledged, "Index has not been deleted") 71 | 72 | //Check if the index has been deleted 73 | response, err = client.IndexExists(IndexName) 74 | helper.OK(t, err) 75 | helper.Assert(t, !response, "Index has not been removed with DeleteIndex function") 76 | 77 | } 78 | 79 | func TestCRUD(t *testing.T) { 80 | type Product struct { 81 | Name string 82 | ID string `json:"_id"` 83 | } 84 | 85 | helper := Test{} 86 | client := elasticsearch.NewClient(ESScheme, ESHost, ESPort) 87 | //Create the index 88 | client.CreateIndex(IndexName, IndexMapping) 89 | 90 | item := Product{Name: "Jeans", ID: "1234"} 91 | 92 | jsonProduct, err := json.Marshal(item) 93 | helper.OK(t, err) 94 | 95 | //Insert 96 | insertResponse, err := client.InsertDocument(IndexName, ProductDocumentType, item.ID, jsonProduct) 97 | helper.OK(t, err) 98 | helper.Assert(t, insertResponse.ID == "1234", "The document has not been inserted") 99 | 100 | version := insertResponse.Version 101 | 102 | //Update 103 | item.Name = "Polo" 104 | insertResponse, err = client.InsertDocument(IndexName, ProductDocumentType, item.ID, jsonProduct) 105 | helper.OK(t, err) 106 | helper.Assert(t, insertResponse.Version == version+1, "The document has not been updated") 107 | 108 | //Read 109 | readResponse, err := client.Document(IndexName, ProductDocumentType, item.ID) 110 | helper.OK(t, err) 111 | helper.Assert(t, readResponse.Found, "The document has not been found") 112 | 113 | var p Product 114 | err = json.Unmarshal(readResponse.Source, &p) 115 | helper.OK(t, err) 116 | helper.Assert(t, p.ID == "1234", "The document has not been retreived") 117 | 118 | //Delete 119 | delResponse, err := client.DeleteDocument(IndexName, ProductDocumentType, item.ID) 120 | helper.OK(t, err) 121 | helper.Assert(t, delResponse.Found, "The document has not beem deleted") 122 | 123 | //Delete the index 124 | deleteResponse, err := client.DeleteIndex(IndexName) 125 | helper.OK(t, err) 126 | helper.Assert(t, deleteResponse.Acknowledged, "Index has not been deleted") 127 | } 128 | 129 | func TestSearch(t *testing.T) { 130 | type Product struct { 131 | Name string 132 | Colors []string 133 | ID string `json:"_id"` 134 | } 135 | 136 | products := [...]Product{ 137 | Product{Name: "Jeans", ID: "1", Colors: []string{"blue", "red"}}, 138 | Product{Name: "Polo", ID: "2", Colors: []string{"yellow", "red"}}, 139 | Product{Name: "Shirt", ID: "3", Colors: []string{"brown", "blue"}}, 140 | } 141 | helper := Test{} 142 | client := elasticsearch.NewClient(ESScheme, ESHost, ESPort) 143 | client.CreateIndex(IndexName, IndexMapping) 144 | 145 | //Bulk 146 | var buffer bytes.Buffer 147 | for _, value := range products { 148 | buffer.WriteString(BulkIndexConstant(IndexName, ProductDocumentType, value.ID)) 149 | buffer.WriteByte('\n') 150 | 151 | jsonProduct, err := json.Marshal(value) 152 | helper.OK(t, err) 153 | buffer.Write(jsonProduct) 154 | buffer.WriteByte('\n') 155 | } 156 | 157 | _, err := client.Bulk(IndexName, buffer.Bytes()) 158 | helper.OK(t, err) 159 | 160 | //We have to wait after a bulk 161 | time.Sleep(1500 * time.Millisecond) 162 | 163 | //Search 164 | search, err := client.Search(IndexName, ProductDocumentType, SearchByColorQuery("red"), false) 165 | helper.OK(t, err) 166 | helper.Assert(t, search.Hits.Total.Value == 2, "The search doesn't return all matched items") 167 | 168 | //MSearch 169 | 170 | mqueries := make([]elasticsearch.MSearchQuery, 2) 171 | mqueries[0] = elasticsearch.MSearchQuery{Header: `{ "index":` + IndexName + `, "type":"` + ProductDocumentType + `" }`, Body: `{ "query": {"match_all" : {}}, "from" : 0, "size" : 1} }`} 172 | mqueries[1] = elasticsearch.MSearchQuery{Header: `{ "index":` + IndexName + `, "type":"` + ProductDocumentType + `" }`, Body: `{"query": {"match_all" : {}}, "from" : 0, "size" : 2}}`} 173 | 174 | msresult, err := client.MSearch(mqueries) 175 | helper.OK(t, err) 176 | helper.Assert(t, msresult.Responses[0].Hits.Total.Value == 1, "The msearch doesn't return all matched items") 177 | helper.Assert(t, msresult.Responses[1].Hits.Total.Value == 2, "The msearch doesn't return all matched items") 178 | 179 | //Delete the index 180 | deleteResponse, err := client.DeleteIndex(IndexName) 181 | helper.OK(t, err) 182 | helper.Assert(t, deleteResponse.Acknowledged, "Index has not been deleted") 183 | } 184 | 185 | func BulkIndexConstant(indexName, documentType, id string) string { 186 | 187 | return `{"index": 188 | { "_index": "` + indexName + `", 189 | "_type": "` + documentType + `", 190 | "_id": "` + id + `" 191 | } 192 | }` 193 | } 194 | 195 | func SearchByColorQuery(color string) string { 196 | return `{ 197 | "query": { 198 | "match": { 199 | "Colors": "` + color + `" 200 | } 201 | } 202 | }` 203 | } 204 | 205 | func TestSuggestion(t *testing.T) { 206 | 207 | type PayLoadSuggester struct { 208 | ID string `json:"id"` 209 | SKU string `json:"sku"` 210 | } 211 | 212 | type InputSuggester struct { 213 | Input []string `json:"input"` 214 | Ouput string `json:"output"` 215 | Payload PayLoadSuggester `json:"payload"` 216 | } 217 | 218 | type SuggestionItem struct { 219 | Name InputSuggester `json:"name_suggest"` 220 | } 221 | 222 | type OutputSuggester struct { 223 | Text string `json:"text"` 224 | Score float32 `json:"score"` 225 | Payload PayLoadSuggester `json:"payload"` 226 | } 227 | 228 | type SuggestionResult struct { 229 | Shards struct { 230 | Total int `json:"total"` 231 | Successful int `json:"successful"` 232 | Failed int `json:"failed"` 233 | } `json:"_shards"` 234 | Suggestion []struct { 235 | Text string `json:"text"` 236 | Offset float32 `json:"offset"` 237 | Lenght int `json:"length"` 238 | Options []OutputSuggester `json:"options"` 239 | } `json:"suggestion"` 240 | } 241 | 242 | helper := Test{} 243 | client := elasticsearch.NewClient(ESScheme, ESHost, ESPort) 244 | client.CreateIndex(SuggestionIndexName, SuggestionIndexMapping) 245 | 246 | //Add Data 247 | sugg := &SuggestionItem{} 248 | sugg.Name = InputSuggester{} 249 | sugg.Name.Input = []string{"jeans", "Levi's jeans", "Levi's"} 250 | sugg.Name.Ouput = "Levi's jeans" 251 | sugg.Name.Payload = PayLoadSuggester{"12345", "HJYSTG"} 252 | 253 | jsonSuggestion, err := json.Marshal(sugg) 254 | helper.OK(t, err) 255 | 256 | //Insert 257 | insertResponse, err := client.InsertDocument(SuggestionIndexName, "suggestion", "1234", jsonSuggestion) 258 | helper.OK(t, err) 259 | helper.Assert(t, insertResponse.ID == "1234", "The document has not been inserted") 260 | 261 | //Suggest 262 | suggestResponse, err := client.Suggest(SuggestionIndexName, SuggestByTermQuery("jean")) 263 | helper.OK(t, err) 264 | 265 | var s SuggestionResult 266 | err = json.Unmarshal(suggestResponse, &s) 267 | helper.OK(t, err) 268 | helper.Assert(t, s.Shards.Failed == 0, "No suggestion inserted") 269 | 270 | //Delete the index 271 | deleteResponse, err := client.DeleteIndex(SuggestionIndexName) 272 | helper.OK(t, err) 273 | helper.Assert(t, deleteResponse.Acknowledged, "Index has not been deleted") 274 | 275 | } 276 | 277 | func SuggestByTermQuery(term string) string { 278 | return `{ 279 | "suggestion" : { 280 | "text" : "` + term + `", 281 | "completion" : { 282 | "field" : "name_suggest" 283 | } 284 | } 285 | }` 286 | } 287 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maximelamure/elasticsearch 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package elasticsearch_test 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | //https://github.com/benbjohnson/testing 12 | 13 | // Test represents a set of helper functions to test 14 | type Test struct{} 15 | 16 | // Assert fails the test if the condition is false. 17 | func (t *Test) Assert(tb testing.TB, condition bool, msg string, v ...interface{}) { 18 | if !condition { 19 | _, file, line, _ := runtime.Caller(1) 20 | fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) 21 | tb.FailNow() 22 | } 23 | } 24 | 25 | // OK fails the test if an err is not nil. 26 | func (t *Test) OK(tb testing.TB, err error) { 27 | if err != nil { 28 | _, file, line, _ := runtime.Caller(1) 29 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 30 | tb.FailNow() 31 | } 32 | } 33 | 34 | // Equals fails the test if exp is not equal to act. 35 | func (t *Test) Equals(tb testing.TB, exp, act interface{}) { 36 | if !reflect.DeepEqual(exp, act) { 37 | _, file, line, _ := runtime.Caller(1) 38 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) 39 | tb.FailNow() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /structs.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import "encoding/json" 4 | 5 | // Response represents a boolean response sent back by the search egine 6 | type Response struct { 7 | Acknowledged bool 8 | Error string 9 | Status int 10 | } 11 | 12 | // Settings represents the mapping structure of one or several indices 13 | type Settings struct { 14 | Shards map[string]interface{} `json:"_shards"` 15 | Indices map[string]interface{} `json:"indices"` 16 | } 17 | 18 | // Status represents the status of the search engine 19 | type Status struct { 20 | TagLine string 21 | Version struct { 22 | Number string 23 | BuildHash string `json:"build_hash"` 24 | BuildTimestamp string `json:"build_timestamp"` 25 | BuildSnapshot bool `json:"build_snapshot"` 26 | LuceneVersion string `json:"lucene_version"` 27 | } 28 | Name string 29 | Status int 30 | Ok bool 31 | } 32 | 33 | // InsertDocument represents the result of the insert operation of a document 34 | type InsertDocument struct { 35 | Created bool `json:"created"` 36 | Index string `json:"_index"` 37 | Type string `json:"_type"` 38 | ID string `json:"_id"` 39 | Version int `json:"_version"` 40 | } 41 | 42 | // Document represents a document 43 | type Document struct { 44 | Index string `json:"_index"` 45 | Type string `json:"_type"` 46 | ID string `json:"_id"` 47 | Version int `json:"_version"` 48 | Found bool `json:"found"` 49 | Source json.RawMessage `json:"_source"` 50 | } 51 | 52 | // Bulk represents the result of the Bulk operation 53 | type Bulk struct { 54 | Took uint64 `json:"took"` 55 | Errors bool `json:"errors"` 56 | Items []struct { 57 | Create struct { 58 | Index string `json:"_index"` 59 | Type string `json:"_type"` 60 | ID string `json:"_id"` 61 | Status int `json:"status"` 62 | Error string `json:"error"` 63 | } `json:"create"` 64 | Index struct { 65 | Index string `json:"_index"` 66 | Type string `json:"_type"` 67 | ID string `json:"_id"` 68 | Version int `json:"_version"` 69 | Status int `json:"status"` 70 | Error struct { 71 | Type string `json:"status"` 72 | Reason string `json:"reason"` 73 | Index_UUID string `json:"index_uuid"` 74 | Shard string `json:"shard"` 75 | Index string `json:"index"` 76 | } `json:"error"` 77 | } `json:"index"` 78 | } `json:"items"` 79 | } 80 | 81 | // SearchResult represents the result of the search operation 82 | type SearchResult struct { 83 | Took uint64 `json:"took"` 84 | TimedOut bool `json:"timed_out"` 85 | Shards struct { 86 | Total int `json:"total"` 87 | Successful int `json:"successful"` 88 | Skipped int `json:"skipped"` 89 | Failed int `json:"failed"` 90 | } `json:"_shards"` 91 | Hits ResultHits `json:"hits"` 92 | Aggregations json.RawMessage `json:"aggregations"` 93 | } 94 | 95 | // ResultHits represents the result of the search hits 96 | type ResultHits struct { 97 | Total struct { 98 | Value int `json:"value"` 99 | Relation string `json:"relation"` 100 | } `json:"total"` 101 | MaxScore float32 `json:"max_score"` 102 | Hits []Hit `json:"hits"` 103 | } 104 | 105 | type Hit struct { 106 | Index string `json:"_index"` 107 | Type string `json:"_type"` 108 | ID string `json:"_id"` 109 | Score float32 `json:"_score"` 110 | Source json.RawMessage `json:"_source"` 111 | Highlight map[string][]string `json:"highlight,omitempty"` 112 | } 113 | 114 | // MSearchQuery Multi Search query 115 | type MSearchQuery struct { 116 | Header string // index name, document type 117 | Body string // query related to the declared index 118 | } 119 | 120 | // MSearchResult Multi search result 121 | type MSearchResult struct { 122 | Responses []SearchResult `json:"responses"` 123 | } 124 | 125 | type UpdateByQueryResult struct { 126 | Took int `json:"took"` 127 | TimedOut bool `json:"timed_out"` 128 | Total int `json:"total"` 129 | Updated int `json:"updated"` 130 | Deleted int `json:"deleted"` 131 | Batches int `json:"batches"` 132 | VersionConflicts int `json:"version_conflicts"` 133 | Noops int `json:"noops"` 134 | Retries struct { 135 | Bulk int `json:"bulk"` 136 | Search int `json:"search"` 137 | } `json:"retries"` 138 | Failures []interface{} `json:"failures"` 139 | } 140 | --------------------------------------------------------------------------------