Browse Source

Backport changes from v5

Oliver Eilhard 2 years ago
parent
commit
ad643b571a

+ 1
- 0
.travis.yml.off View File

@@ -1,5 +1,6 @@
1 1
 sudo: required
2 2
 language: go
3
+script: go test -race -v . ./config
3 4
 go:
4 5
   - 1.7
5 6
   - 1.8

+ 8
- 0
CONTRIBUTORS View File

@@ -15,8 +15,10 @@ Alex [@akotlar](https://github.com/akotlar)
15 15
 Alexandre Olivier [@aliphen](https://github.com/aliphen)
16 16
 Alexey Sharov [@nizsheanez](https://github.com/nizsheanez)
17 17
 AndreKR [@AndreKR](https://github.com/AndreKR)
18
+André Bierlein [@ligustah](https://github.com/ligustah)
18 19
 Andrew Dunham [@andrew-d](https://github.com/andrew-d)
19 20
 Andrew Gaul [@andrewgaul](https://github.com/andrewgaul)
21
+Andy Walker [@alaska](https://github.com/alaska)
20 22
 Arquivei [@arquivei](https://github.com/arquivei)
21 23
 Benjamin Fernandes [@LotharSee](https://github.com/LotharSee)
22 24
 Benjamin Zarzycki [@kf6nux](https://github.com/kf6nux)
@@ -26,7 +28,9 @@ Bryan Conklin [@bmconklin](https://github.com/bmconklin)
26 28
 Bruce Zhou [@brucez-isell](https://github.com/brucez-isell)
27 29
 cforbes [@cforbes](https://github.com/cforbes)
28 30
 Chris M [@tebriel](https://github.com/tebriel)
31
+Chris Rice [@donutmonger](https://github.com/donutmonger)
29 32
 Christophe Courtaut [@kri5](https://github.com/kri5)
33
+Connor Peet [@connor4312](https://github.com/connor4312)
30 34
 Conrad Pankoff [@deoxxa](https://github.com/deoxxa)
31 35
 Corey Scott [@corsc](https://github.com/corsc)
32 36
 Daniel Barrett [@shendaras](https://github.com/shendaras)
@@ -59,6 +63,7 @@ John Goodall [@jgoodall](https://github.com/jgoodall)
59 63
 John Stanford [@jxstanford](https://github.com/jxstanford)
60 64
 jun [@coseyo](https://github.com/coseyo)
61 65
 Junpei Tsuji [@jun06t](https://github.com/jun06t)
66
+Keith Hatton [@khatton-ft](https://github.com/khatton-ft)
62 67
 Kenta SUZUKI [@suzuken](https://github.com/suzuken)
63 68
 Kevin Mulvey [@kmulvey](https://github.com/kmulvey)
64 69
 Kyle Brandt [@kylebrandt](https://github.com/kylebrandt)
@@ -77,6 +82,7 @@ Naoya Tsutsumi [@tutuming](https://github.com/tutuming)
77 82
 Nicholas Wolff [@nwolff](https://github.com/nwolff)
78 83
 Nick K [@utrack](https://github.com/utrack)
79 84
 Nick Whyte [@nickw444](https://github.com/nickw444)
85
+Nicolae Vartolomei [@nvartolomei](https://github.com/nvartolomei)
80 86
 Orne Brocaar [@brocaar](https://github.com/brocaar)
81 87
 Pete C [@peteclark-ft](https://github.com/peteclark-ft)
82 88
 Radoslaw Wesolowski [r--w](https://github.com/r--w)
@@ -88,6 +94,7 @@ Stephen Kubovic [@stephenkubovic](https://github.com/stephenkubovic)
88 94
 Stuart Warren [@Woz](https://github.com/stuart-warren)
89 95
 Sulaiman [@salajlan](https://github.com/salajlan)
90 96
 Sundar [@sundarv85](https://github.com/sundarv85)
97
+Swarlston [@Swarlston](https://github.com/Swarlston)
91 98
 Take [ww24](https://github.com/ww24)
92 99
 Tetsuya Morimoto [@t2y](https://github.com/t2y)
93 100
 TimeEmit [@TimeEmit](https://github.com/timeemit)
@@ -99,3 +106,4 @@ Wyndham Blanton [@wyndhblb](https://github.com/wyndhblb)
99 106
 Yarden Bar [@ayashjorden](https://github.com/ayashjorden)
100 107
 zakthomas [@zakthomas](https://github.com/zakthomas)
101 108
 singham [@zhaochenxiao90](https://github.com/zhaochenxiao90)
109
+Roman Colohanin [@zuzmic](https://github.com/zuzmic)

+ 53
- 3
client.go View File

@@ -9,15 +9,19 @@ import (
9 9
 	"context"
10 10
 	"encoding/json"
11 11
 	"fmt"
12
+	"log"
12 13
 	"net/http"
13 14
 	"net/http/httputil"
14 15
 	"net/url"
16
+	"os"
15 17
 	"regexp"
16 18
 	"strings"
17 19
 	"sync"
18 20
 	"time"
19 21
 
20 22
 	"github.com/pkg/errors"
23
+
24
+	"gopkg.in/olivere/elastic.v5/config"
21 25
 )
22 26
 
23 27
 const (
@@ -288,6 +292,47 @@ func NewClient(options ...ClientOptionFunc) (*Client, error) {
288 292
 	return c, nil
289 293
 }
290 294
 
295
+// NewClientFromConfig initializes a client from a configuration.
296
+func NewClientFromConfig(cfg *config.Config) (*Client, error) {
297
+	var options []ClientOptionFunc
298
+	if cfg != nil {
299
+		if cfg.URL != "" {
300
+			options = append(options, SetURL(cfg.URL))
301
+		}
302
+		if cfg.Errorlog != "" {
303
+			f, err := os.OpenFile(cfg.Errorlog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
304
+			if err != nil {
305
+				return nil, errors.Wrap(err, "unable to initialize error log")
306
+			}
307
+			l := log.New(f, "", 0)
308
+			options = append(options, SetErrorLog(l))
309
+		}
310
+		if cfg.Tracelog != "" {
311
+			f, err := os.OpenFile(cfg.Tracelog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
312
+			if err != nil {
313
+				return nil, errors.Wrap(err, "unable to initialize trace log")
314
+			}
315
+			l := log.New(f, "", 0)
316
+			options = append(options, SetTraceLog(l))
317
+		}
318
+		if cfg.Infolog != "" {
319
+			f, err := os.OpenFile(cfg.Infolog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
320
+			if err != nil {
321
+				return nil, errors.Wrap(err, "unable to initialize info log")
322
+			}
323
+			l := log.New(f, "", 0)
324
+			options = append(options, SetInfoLog(l))
325
+		}
326
+		if cfg.Username != "" || cfg.Password != "" {
327
+			options = append(options, SetBasicAuth(cfg.Username, cfg.Password))
328
+		}
329
+		if cfg.Sniff != nil {
330
+			options = append(options, SetSniff(*cfg.Sniff))
331
+		}
332
+	}
333
+	return NewClient(options...)
334
+}
335
+
291 336
 // NewSimpleClient creates a new short-lived Client that can be used in
292 337
 // use cases where you need e.g. one client per request.
293 338
 //
@@ -1233,6 +1278,9 @@ func (c *Client) PerformRequest(ctx context.Context, method, path string, params
1233 1278
 			defer res.Body.Close()
1234 1279
 		}
1235 1280
 
1281
+		// Tracing
1282
+		c.dumpResponse(res)
1283
+
1236 1284
 		// Check for errors
1237 1285
 		if err := checkResponse((*http.Request)(req), res, ignoreErrors...); err != nil {
1238 1286
 			// No retry if request succeeded
@@ -1241,9 +1289,6 @@ func (c *Client) PerformRequest(ctx context.Context, method, path string, params
1241 1289
 			return resp, err
1242 1290
 		}
1243 1291
 
1244
-		// Tracing
1245
-		c.dumpResponse(res)
1246
-
1247 1292
 		// We successfully made a request with this connection
1248 1293
 		conn.MarkAsHealthy()
1249 1294
 
@@ -1620,6 +1665,11 @@ func (c *Client) TasksList() *TasksListService {
1620 1665
 	return NewTasksListService(c)
1621 1666
 }
1622 1667
 
1668
+// TasksGetTask retrieves a task running on the cluster.
1669
+func (c *Client) TasksGetTask() *TasksGetTaskService {
1670
+	return NewTasksGetTaskService(c)
1671
+}
1672
+
1623 1673
 // TODO Pending cluster tasks
1624 1674
 // TODO Cluster Reroute
1625 1675
 // TODO Cluster Update Settings

+ 31
- 1
client_test.go View File

@@ -16,6 +16,7 @@ import (
16 16
 	"reflect"
17 17
 	"regexp"
18 18
 	"strings"
19
+	"sync"
19 20
 	"testing"
20 21
 	"time"
21 22
 
@@ -294,6 +295,7 @@ func TestClientHealthcheckTimeoutLeak(t *testing.T) {
294 295
 	// leaks via leaktest.
295 296
 	mux := http.NewServeMux()
296 297
 
298
+	var reqDoneMu sync.Mutex
297 299
 	var reqDone bool
298 300
 	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
299 301
 		cn, ok := w.(http.CloseNotifier)
@@ -301,7 +303,9 @@ func TestClientHealthcheckTimeoutLeak(t *testing.T) {
301 303
 			t.Fatalf("Writer is not CloseNotifier, but %v", reflect.TypeOf(w).Name())
302 304
 		}
303 305
 		<-cn.CloseNotify()
306
+		reqDoneMu.Lock()
304 307
 		reqDone = true
308
+		reqDoneMu.Unlock()
305 309
 	})
306 310
 
307 311
 	lis, err := net.Listen("tcp", "127.0.0.1:0")
@@ -346,9 +350,12 @@ func TestClientHealthcheckTimeoutLeak(t *testing.T) {
346 350
 	}
347 351
 
348 352
 	<-time.After(time.Second)
353
+	reqDoneMu.Lock()
349 354
 	if !reqDone {
355
+		reqDoneMu.Unlock()
350 356
 		t.Fatal("Request wasn't canceled or stopped")
351 357
 	}
358
+	reqDoneMu.Unlock()
352 359
 }
353 360
 
354 361
 // -- NewSimpleClient --
@@ -552,6 +559,7 @@ func TestClientSniffTimeoutLeak(t *testing.T) {
552 559
 	// leaks via leaktest.
553 560
 	mux := http.NewServeMux()
554 561
 
562
+	var reqDoneMu sync.Mutex
555 563
 	var reqDone bool
556 564
 	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
557 565
 		cn, ok := w.(http.CloseNotifier)
@@ -559,7 +567,9 @@ func TestClientSniffTimeoutLeak(t *testing.T) {
559 567
 			t.Fatalf("Writer is not CloseNotifier, but %v", reflect.TypeOf(w).Name())
560 568
 		}
561 569
 		<-cn.CloseNotify()
570
+		reqDoneMu.Lock()
562 571
 		reqDone = true
572
+		reqDoneMu.Unlock()
563 573
 	})
564 574
 
565 575
 	lis, err := net.Listen("tcp", "127.0.0.1:0")
@@ -605,9 +615,12 @@ func TestClientSniffTimeoutLeak(t *testing.T) {
605 615
 	}
606 616
 
607 617
 	<-time.After(time.Second)
618
+	reqDoneMu.Lock()
608 619
 	if !reqDone {
620
+		reqDoneMu.Unlock()
609 621
 		t.Fatal("Request wasn't canceled or stopped")
610 622
 	}
623
+	reqDoneMu.Unlock()
611 624
 }
612 625
 
613 626
 func TestClientExtractHostname(t *testing.T) {
@@ -973,6 +986,22 @@ func TestPerformRequestWithLoggerAndTracer(t *testing.T) {
973 986
 		t.Errorf("expected tracer output; got: %q", tgot)
974 987
 	}
975 988
 }
989
+func TestPerformRequestWithTracerOnError(t *testing.T) {
990
+	var tw bytes.Buffer
991
+	tout := log.New(&tw, "TRACER ", log.LstdFlags)
992
+
993
+	client, err := NewClient(SetTraceLog(tout), SetSniff(false))
994
+	if err != nil {
995
+		t.Fatal(err)
996
+	}
997
+
998
+	client.PerformRequest(context.TODO(), "GET", "/no-such-index", nil, nil)
999
+
1000
+	tgot := tw.String()
1001
+	if tgot == "" {
1002
+		t.Errorf("expected tracer output; got: %q", tgot)
1003
+	}
1004
+}
976 1005
 
977 1006
 type customLogger struct {
978 1007
 	out bytes.Buffer
@@ -1179,7 +1208,8 @@ func TestPerformRequestWithTimeout(t *testing.T) {
1179 1208
 		res *Response
1180 1209
 		err error
1181 1210
 	}
1182
-	ctx, _ := context.WithTimeout(context.Background(), 1*time.Second)
1211
+	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
1212
+	defer cancel()
1183 1213
 
1184 1214
 	resc := make(chan result, 1)
1185 1215
 	go func() {

+ 90
- 0
config/config.go View File

@@ -0,0 +1,90 @@
1
+// Copyright 2012-present Oliver Eilhard. All rights reserved.
2
+// Use of this source code is governed by a MIT-license.
3
+// See http://olivere.mit-license.org/license.txt for details.
4
+
5
+package config
6
+
7
+import (
8
+	"fmt"
9
+	"net/url"
10
+	"strconv"
11
+	"strings"
12
+)
13
+
14
+// Config represents an Elasticsearch configuration.
15
+type Config struct {
16
+	URL      string
17
+	Index    string
18
+	Username string
19
+	Password string
20
+	Shards   int
21
+	Replicas int
22
+	Sniff    *bool
23
+	Infolog  string
24
+	Errorlog string
25
+	Tracelog string
26
+}
27
+
28
+// Parse returns the Elasticsearch configuration by extracting it
29
+// from the URL, its path, and its query string.
30
+//
31
+// Example:
32
+//   http://127.0.0.1:9200/store-blobs?shards=1&replicas=0&sniff=false&tracelog=elastic.trace.log
33
+//
34
+// The code above will return a URL of http://127.0.0.1:9200, an index name
35
+// of store-blobs, and the related settings from the query string.
36
+func Parse(elasticURL string) (*Config, error) {
37
+	cfg := &Config{
38
+		Shards:   1,
39
+		Replicas: 0,
40
+		Sniff:    nil,
41
+	}
42
+
43
+	uri, err := url.Parse(elasticURL)
44
+	if err != nil {
45
+		return nil, fmt.Errorf("error parsing elastic parameter %q: %v", elasticURL, err)
46
+	}
47
+	index := uri.Path
48
+	if strings.HasPrefix(index, "/") {
49
+		index = index[1:]
50
+	}
51
+	if strings.HasSuffix(index, "/") {
52
+		index = index[:len(index)-1]
53
+	}
54
+	if index == "" {
55
+		return nil, fmt.Errorf("missing index in elastic parameter %q", elasticURL)
56
+	}
57
+	if uri.User != nil {
58
+		cfg.Username = uri.User.Username()
59
+		cfg.Password, _ = uri.User.Password()
60
+	}
61
+	uri.User = nil
62
+
63
+	if i, err := strconv.Atoi(uri.Query().Get("shards")); err == nil {
64
+		cfg.Shards = i
65
+	}
66
+	if i, err := strconv.Atoi(uri.Query().Get("replicas")); err == nil {
67
+		cfg.Replicas = i
68
+	}
69
+	if s := uri.Query().Get("sniff"); s != "" {
70
+		if b, err := strconv.ParseBool(s); err == nil {
71
+			cfg.Sniff = &b
72
+		}
73
+	}
74
+	if s := uri.Query().Get("infolog"); s != "" {
75
+		cfg.Infolog = s
76
+	}
77
+	if s := uri.Query().Get("errorlog"); s != "" {
78
+		cfg.Errorlog = s
79
+	}
80
+	if s := uri.Query().Get("tracelog"); s != "" {
81
+		cfg.Tracelog = s
82
+	}
83
+
84
+	uri.Path = ""
85
+	uri.RawQuery = ""
86
+	cfg.URL = uri.String()
87
+	cfg.Index = index
88
+
89
+	return cfg, nil
90
+}

+ 45
- 0
config/config_test.go View File

@@ -0,0 +1,45 @@
1
+// Copyright 2012-present Oliver Eilhard. All rights reserved.
2
+// Use of this source code is governed by a MIT-license.
3
+// See http://olivere.mit-license.org/license.txt for details.
4
+
5
+package config
6
+
7
+import "testing"
8
+
9
+func TestParse(t *testing.T) {
10
+	urls := "http://user:pwd@elastic:19220/store-blobs?shards=5&replicas=2&sniff=true&errorlog=elastic.error.log&infolog=elastic.info.log&tracelog=elastic.trace.log"
11
+	cfg, err := Parse(urls)
12
+	if err != nil {
13
+		t.Fatal(err)
14
+	}
15
+	if want, got := "http://elastic:19220", cfg.URL; want != got {
16
+		t.Fatalf("expected URL = %q, got %q", want, got)
17
+	}
18
+	if want, got := "store-blobs", cfg.Index; want != got {
19
+		t.Fatalf("expected Index = %q, got %q", want, got)
20
+	}
21
+	if want, got := "user", cfg.Username; want != got {
22
+		t.Fatalf("expected Username = %q, got %q", want, got)
23
+	}
24
+	if want, got := "pwd", cfg.Password; want != got {
25
+		t.Fatalf("expected Password = %q, got %q", want, got)
26
+	}
27
+	if want, got := 5, cfg.Shards; want != got {
28
+		t.Fatalf("expected Shards = %v, got %v", want, got)
29
+	}
30
+	if want, got := 2, cfg.Replicas; want != got {
31
+		t.Fatalf("expected Replicas = %v, got %v", want, got)
32
+	}
33
+	if want, got := true, *cfg.Sniff; want != got {
34
+		t.Fatalf("expected Sniff = %v, got %v", want, got)
35
+	}
36
+	if want, got := "elastic.error.log", cfg.Errorlog; want != got {
37
+		t.Fatalf("expected Errorlog = %q, got %q", want, got)
38
+	}
39
+	if want, got := "elastic.info.log", cfg.Infolog; want != got {
40
+		t.Fatalf("expected Infolog = %q, got %q", want, got)
41
+	}
42
+	if want, got := "elastic.trace.log", cfg.Tracelog; want != got {
43
+		t.Fatalf("expected Tracelog = %q, got %q", want, got)
44
+	}
45
+}

+ 9
- 0
config/doc.go View File

@@ -0,0 +1,9 @@
1
+// Copyright 2012-present Oliver Eilhard. All rights reserved.
2
+// Use of this source code is governed by a MIT-license.
3
+// See http://olivere.mit-license.org/license.txt for details.
4
+
5
+/*
6
+Package config allows parsing a configuration for Elasticsearch
7
+from a URL.
8
+*/
9
+package config

+ 12
- 3
delete.go View File

@@ -7,6 +7,7 @@ package elastic
7 7
 import (
8 8
 	"context"
9 9
 	"fmt"
10
+	"net/http"
10 11
 	"net/url"
11 12
 
12 13
 	"gopkg.in/olivere/elastic.v6/uritemplates"
@@ -168,7 +169,9 @@ func (s *DeleteService) Validate() error {
168 169
 	return nil
169 170
 }
170 171
 
171
-// Do executes the operation.
172
+// Do executes the operation. If the document is not found (404), Elasticsearch will
173
+// still return a response. This response is serialized and returned as well. In other
174
+// words, for HTTP status code 404, both an error and a response might be returned.
172 175
 func (s *DeleteService) Do(ctx context.Context) (*DeleteResponse, error) {
173 176
 	// Check pre-conditions
174 177
 	if err := s.Validate(); err != nil {
@@ -182,7 +185,7 @@ func (s *DeleteService) Do(ctx context.Context) (*DeleteResponse, error) {
182 185
 	}
183 186
 
184 187
 	// Get HTTP response
185
-	res, err := s.client.PerformRequest(ctx, "DELETE", path, params, nil)
188
+	res, err := s.client.PerformRequest(ctx, "DELETE", path, params, nil, http.StatusNotFound)
186 189
 	if err != nil {
187 190
 		return nil, err
188 191
 	}
@@ -192,6 +195,12 @@ func (s *DeleteService) Do(ctx context.Context) (*DeleteResponse, error) {
192 195
 	if err := s.client.decoder.Decode(res.Body, ret); err != nil {
193 196
 		return nil, err
194 197
 	}
198
+
199
+	// If we have a 404, we return both a result and an error, just like ES does
200
+	if res.StatusCode == http.StatusNotFound {
201
+		return ret, &Error{Status: http.StatusNotFound}
202
+	}
203
+
195 204
 	return ret, nil
196 205
 }
197 206
 
@@ -204,7 +213,7 @@ type DeleteResponse struct {
204 213
 	Id            string      `json:"_id"`
205 214
 	Version       int64       `json:"_version"`
206 215
 	Shards        *shardsInfo `json:"_shards"`
207
-	Result        bool        `json:"string,omitempty"`
216
+	Result        string      `json:"result,omitempty"`
208 217
 	ForcedRefresh bool        `json:"forced_refresh,omitempty"`
209 218
 	Found         bool        `json:"found"`
210 219
 }

+ 12
- 11
delete_by_query.go View File

@@ -614,23 +614,24 @@ func (s *DeleteByQueryService) Do(ctx context.Context) (*BulkIndexByScrollRespon
614 614
 // BulkIndexByScrollResponse is the outcome of executing Do with
615 615
 // DeleteByQueryService and UpdateByQueryService.
616 616
 type BulkIndexByScrollResponse struct {
617
-	Took             int64 `json:"took"`
618
-	TimedOut         bool  `json:"timed_out"`
619
-	Total            int64 `json:"total"`
620
-	Updated          int64 `json:"updated"`
621
-	Created          int64 `json:"created"`
622
-	Deleted          int64 `json:"deleted"`
623
-	Batches          int64 `json:"batches"`
624
-	VersionConflicts int64 `json:"version_conflicts"`
625
-	Noops            int64 `json:"noops"`
617
+	Took             int64  `json:"took"`
618
+	SliceId          *int64 `json:"slice_id,omitempty"`
619
+	TimedOut         bool   `json:"timed_out"`
620
+	Total            int64  `json:"total"`
621
+	Updated          int64  `json:"updated,omitempty"`
622
+	Created          int64  `json:"created,omitempty"`
623
+	Deleted          int64  `json:"deleted"`
624
+	Batches          int64  `json:"batches"`
625
+	VersionConflicts int64  `json:"version_conflicts"`
626
+	Noops            int64  `json:"noops"`
626 627
 	Retries          struct {
627 628
 		Bulk   int64 `json:"bulk"`
628 629
 		Search int64 `json:"search"`
629
-	} `json:"retries"`
630
+	} `json:"retries,omitempty"`
630 631
 	Throttled            string                             `json:"throttled"`
631 632
 	ThrottledMillis      int64                              `json:"throttled_millis"`
632 633
 	RequestsPerSecond    float64                            `json:"requests_per_second"`
633
-	Canceled             string                             `json:"canceled"`
634
+	Canceled             string                             `json:"canceled,omitempty"`
634 635
 	ThrottledUntil       string                             `json:"throttled_until"`
635 636
 	ThrottledUntilMillis int64                              `json:"throttled_until_millis"`
636 637
 	Failures             []bulkIndexByScrollResponseFailure `json:"failures"`

+ 22
- 4
delete_test.go View File

@@ -69,13 +69,31 @@ func TestDelete(t *testing.T) {
69 69
 	// Delete non existent document 99
70 70
 	res, err = client.Delete().Index(testIndexName).Type("tweet").Id("99").Refresh("true").Do(context.TODO())
71 71
 	if err == nil {
72
-		t.Fatalf("expected error; got: %v", err)
72
+		t.Fatal("expected error")
73 73
 	}
74 74
 	if !IsNotFound(err) {
75
-		t.Errorf("expected NotFound error; got %v", err)
75
+		t.Fatalf("expected 404, got: %v", err)
76 76
 	}
77
-	if res != nil {
78
-		t.Fatalf("expected no response; got: %v", res)
77
+	if _, ok := err.(*Error); !ok {
78
+		t.Fatalf("expected error type *Error, got: %T", err)
79
+	}
80
+	if res == nil {
81
+		t.Fatal("expected response")
82
+	}
83
+	if res.Found {
84
+		t.Errorf("expected Found = false; got %v", res.Found)
85
+	}
86
+	if have, want := res.Id, "99"; have != want {
87
+		t.Errorf("expected _id = %q, got %q", have, want)
88
+	}
89
+	if have, want := res.Index, testIndexName; have != want {
90
+		t.Errorf("expected _index = %q, got %q", have, want)
91
+	}
92
+	if have, want := res.Type, "tweet"; have != want {
93
+		t.Errorf("expected _type = %q, got %q", have, want)
94
+	}
95
+	if have, want := res.Result, "not_found"; have != want {
96
+		t.Errorf("expected result = %q, got %q", have, want)
79 97
 	}
80 98
 
81 99
 	count, err = client.Count(testIndexName).Do(context.TODO())

+ 21
- 15
errors.go View File

@@ -93,32 +93,38 @@ func (e *Error) Error() string {
93 93
 // returned HTTP status 404. The err parameter can be of type *elastic.Error,
94 94
 // elastic.Error, *http.Response or int (indicating the HTTP status code).
95 95
 func IsNotFound(err interface{}) bool {
96
-	switch e := err.(type) {
97
-	case *http.Response:
98
-		return e.StatusCode == http.StatusNotFound
99
-	case *Error:
100
-		return e.Status == http.StatusNotFound
101
-	case Error:
102
-		return e.Status == http.StatusNotFound
103
-	case int:
104
-		return e == http.StatusNotFound
105
-	}
106
-	return false
96
+	return IsStatusCode(err, http.StatusNotFound)
107 97
 }
108 98
 
109 99
 // IsTimeout returns true if the given error indicates that Elasticsearch
110 100
 // returned HTTP status 408. The err parameter can be of type *elastic.Error,
111 101
 // elastic.Error, *http.Response or int (indicating the HTTP status code).
112 102
 func IsTimeout(err interface{}) bool {
103
+	return IsStatusCode(err, http.StatusRequestTimeout)
104
+}
105
+
106
+// IsConflict returns true if the given error indicates that the Elasticsearch
107
+// operation resulted in a version conflict. This can occur in operations like
108
+// `update` or `index` with `op_type=create`. The err parameter can be of
109
+// type *elastic.Error, elastic.Error, *http.Response or int (indicating the
110
+// HTTP status code).
111
+func IsConflict(err interface{}) bool {
112
+	return IsStatusCode(err, http.StatusConflict)
113
+}
114
+
115
+// IsStatusCode returns true if the given error indicates that the Elasticsearch
116
+// operation returned the specified HTTP status code. The err parameter can be of
117
+// type *http.Response, *Error, Error, or int (indicating the HTTP status code).
118
+func IsStatusCode(err interface{}, code int) bool {
113 119
 	switch e := err.(type) {
114 120
 	case *http.Response:
115
-		return e.StatusCode == http.StatusRequestTimeout
121
+		return e.StatusCode == code
116 122
 	case *Error:
117
-		return e.Status == http.StatusRequestTimeout
123
+		return e.Status == code
118 124
 	case Error:
119
-		return e.Status == http.StatusRequestTimeout
125
+		return e.Status == code
120 126
 	case int:
121
-		return e == http.StatusRequestTimeout
127
+		return e == code
122 128
 	}
123 129
 	return false
124 130
 }

+ 93
- 0
errors_test.go View File

@@ -200,3 +200,96 @@ func TestIsTimeout(t *testing.T) {
200 200
 		t.Errorf("expected %v; got: %v", want, got)
201 201
 	}
202 202
 }
203
+
204
+func TestIsConflict(t *testing.T) {
205
+	if got, want := IsConflict(nil), false; got != want {
206
+		t.Errorf("expected %v; got: %v", want, got)
207
+	}
208
+	if got, want := IsConflict(""), false; got != want {
209
+		t.Errorf("expected %v; got: %v", want, got)
210
+	}
211
+	if got, want := IsConflict(200), false; got != want {
212
+		t.Errorf("expected %v; got: %v", want, got)
213
+	}
214
+	if got, want := IsConflict(http.StatusConflict), true; got != want {
215
+		t.Errorf("expected %v; got: %v", want, got)
216
+	}
217
+
218
+	if got, want := IsConflict(&Error{Status: 409}), true; got != want {
219
+		t.Errorf("expected %v; got: %v", want, got)
220
+	}
221
+	if got, want := IsConflict(&Error{Status: 200}), false; got != want {
222
+		t.Errorf("expected %v; got: %v", want, got)
223
+	}
224
+
225
+	if got, want := IsConflict(Error{Status: 409}), true; got != want {
226
+		t.Errorf("expected %v; got: %v", want, got)
227
+	}
228
+	if got, want := IsConflict(Error{Status: 200}), false; got != want {
229
+		t.Errorf("expected %v; got: %v", want, got)
230
+	}
231
+
232
+	if got, want := IsConflict(&http.Response{StatusCode: 409}), true; got != want {
233
+		t.Errorf("expected %v; got: %v", want, got)
234
+	}
235
+	if got, want := IsConflict(&http.Response{StatusCode: 200}), false; got != want {
236
+		t.Errorf("expected %v; got: %v", want, got)
237
+	}
238
+}
239
+
240
+func TestIsStatusCode(t *testing.T) {
241
+	tests := []struct {
242
+		Error interface{}
243
+		Code  int
244
+		Want  bool
245
+	}{
246
+		// #0
247
+		{
248
+			Error: nil,
249
+			Code:  200,
250
+			Want:  false,
251
+		},
252
+		// #1
253
+		{
254
+			Error: "",
255
+			Code:  200,
256
+			Want:  false,
257
+		},
258
+		// #2
259
+		{
260
+			Error: http.StatusConflict,
261
+			Code:  409,
262
+			Want:  true,
263
+		},
264
+		// #3
265
+		{
266
+			Error: http.StatusConflict,
267
+			Code:  http.StatusInternalServerError,
268
+			Want:  false,
269
+		},
270
+		// #4
271
+		{
272
+			Error: &Error{Status: http.StatusConflict},
273
+			Code:  409,
274
+			Want:  true,
275
+		},
276
+		// #5
277
+		{
278
+			Error: Error{Status: http.StatusConflict},
279
+			Code:  409,
280
+			Want:  true,
281
+		},
282
+		// #6
283
+		{
284
+			Error: &http.Response{StatusCode: http.StatusConflict},
285
+			Code:  409,
286
+			Want:  true,
287
+		},
288
+	}
289
+
290
+	for i, tt := range tests {
291
+		if have, want := IsStatusCode(tt.Error, tt.Code), tt.Want; have != want {
292
+			t.Errorf("#%d: have %v, want %v", i, have, want)
293
+		}
294
+	}
295
+}

config/elasticsearch.yml → etc/elasticsearch.yml View File


config/jvm.options → etc/jvm.options View File


config/log4j2.properties → etc/log4j2.properties View File


config/scripts/.gitkeep → etc/scripts/.gitkeep View File


+ 37
- 21
fetch_source_context.go View File

@@ -9,13 +9,20 @@ import (
9 9
 	"strings"
10 10
 )
11 11
 
12
+// FetchSourceContext enables source filtering, i.e. it allows control
13
+// over how the _source field is returned with every hit. It is used
14
+// with various endpoints, e.g. when searching for documents, retrieving
15
+// individual documents, or even updating documents.
16
+//
17
+// See https://www.elastic.co/guide/en/elasticsearch/reference/5.5/search-request-source-filtering.html
18
+// for details.
12 19
 type FetchSourceContext struct {
13
-	fetchSource     bool
14
-	transformSource bool
15
-	includes        []string
16
-	excludes        []string
20
+	fetchSource bool
21
+	includes    []string
22
+	excludes    []string
17 23
 }
18 24
 
25
+// NewFetchSourceContext returns a new FetchSourceContext.
19 26
 func NewFetchSourceContext(fetchSource bool) *FetchSourceContext {
20 27
 	return &FetchSourceContext{
21 28
 		fetchSource: fetchSource,
@@ -24,51 +31,60 @@ func NewFetchSourceContext(fetchSource bool) *FetchSourceContext {
24 31
 	}
25 32
 }
26 33
 
34
+// FetchSource indicates whether to return the _source.
27 35
 func (fsc *FetchSourceContext) FetchSource() bool {
28 36
 	return fsc.fetchSource
29 37
 }
30 38
 
39
+// SetFetchSource specifies whether to return the _source.
31 40
 func (fsc *FetchSourceContext) SetFetchSource(fetchSource bool) {
32 41
 	fsc.fetchSource = fetchSource
33 42
 }
34 43
 
44
+// Include indicates to return specific parts of the _source.
45
+// Wildcards are allowed here.
35 46
 func (fsc *FetchSourceContext) Include(includes ...string) *FetchSourceContext {
36 47
 	fsc.includes = append(fsc.includes, includes...)
37 48
 	return fsc
38 49
 }
39 50
 
51
+// Exclude indicates to exclude specific parts of the _source.
52
+// Wildcards are allowed here.
40 53
 func (fsc *FetchSourceContext) Exclude(excludes ...string) *FetchSourceContext {
41 54
 	fsc.excludes = append(fsc.excludes, excludes...)
42 55
 	return fsc
43 56
 }
44 57
 
45
-func (fsc *FetchSourceContext) TransformSource(transformSource bool) *FetchSourceContext {
46
-	fsc.transformSource = transformSource
47
-	return fsc
48
-}
49
-
58
+// Source returns the JSON-serializable data to be used in a body.
50 59
 func (fsc *FetchSourceContext) Source() (interface{}, error) {
51 60
 	if !fsc.fetchSource {
52 61
 		return false, nil
53 62
 	}
54
-	return map[string]interface{}{
55
-		"includes": fsc.includes,
56
-		"excludes": fsc.excludes,
57
-	}, nil
63
+	if len(fsc.includes) == 0 && len(fsc.excludes) == 0 {
64
+		return true, nil
65
+	}
66
+	src := make(map[string]interface{})
67
+	if len(fsc.includes) > 0 {
68
+		src["includes"] = fsc.includes
69
+	}
70
+	if len(fsc.excludes) > 0 {
71
+		src["excludes"] = fsc.excludes
72
+	}
73
+	return src, nil
58 74
 }
59 75
 
60 76
 // Query returns the parameters in a form suitable for a URL query string.
61 77
 func (fsc *FetchSourceContext) Query() url.Values {
62 78
 	params := url.Values{}
63
-	if !fsc.fetchSource {
79
+	if fsc.fetchSource {
80
+		if len(fsc.includes) > 0 {
81
+			params.Add("_source_include", strings.Join(fsc.includes, ","))
82
+		}
83
+		if len(fsc.excludes) > 0 {
84
+			params.Add("_source_exclude", strings.Join(fsc.excludes, ","))
85
+		}
86
+	} else {
64 87
 		params.Add("_source", "false")
65
-		return params
66
-	}
67
-	if len(fsc.includes) > 0 {
68
-		params.Add("_source_include", strings.Join(fsc.includes, ","))
69
-	}
70
-	if len(fsc.excludes) > 0 {
71
-		params.Add("_source_exclude", strings.Join(fsc.excludes, ","))
72 88
 	}
73 89
 	return params
74 90
 }

+ 2
- 2
fetch_source_context_test.go View File

@@ -54,7 +54,7 @@ func TestFetchSourceContextFetchSource(t *testing.T) {
54 54
 		t.Fatalf("marshaling to JSON failed: %v", err)
55 55
 	}
56 56
 	got := string(data)
57
-	expected := `{"excludes":[],"includes":[]}`
57
+	expected := `true`
58 58
 	if got != expected {
59 59
 		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
60 60
 	}
@@ -71,7 +71,7 @@ func TestFetchSourceContextFetchSourceWithIncludesOnly(t *testing.T) {
71 71
 		t.Fatalf("marshaling to JSON failed: %v", err)
72 72
 	}
73 73
 	got := string(data)
74
-	expected := `{"excludes":[],"includes":["a","b"]}`
74
+	expected := `{"includes":["a","b"]}`
75 75
 	if got != expected {
76 76
 		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
77 77
 	}

+ 1
- 1
get_test.go View File

@@ -45,7 +45,7 @@ func TestGet(t *testing.T) {
45 45
 }
46 46
 
47 47
 func TestGetWithSourceFiltering(t *testing.T) {
48
-	client := setupTestClientAndCreateIndex(t)
48
+	client := setupTestClientAndCreateIndex(t) // , SetTraceLog(log.New(os.Stdout, "", 0)))
49 49
 
50 50
 	tweet1 := tweet{User: "olivere", Message: "Welcome to Golang and Elasticsearch."}
51 51
 	_, err := client.Index().Index(testIndexName).Type("tweet").Id("1").BodyJson(&tweet1).Do(context.TODO())

+ 7
- 7
indices_put_mapping.go View File

@@ -27,7 +27,7 @@ type IndicesPutMappingService struct {
27 27
 	ignoreUnavailable *bool
28 28
 	allowNoIndices    *bool
29 29
 	expandWildcards   string
30
-	ignoreConflicts   *bool
30
+	updateAllTypes    *bool
31 31
 	timeout           string
32 32
 	bodyJson          map[string]interface{}
33 33
 	bodyString        string
@@ -94,10 +94,10 @@ func (s *IndicesPutMappingService) ExpandWildcards(expandWildcards string) *Indi
94 94
 	return s
95 95
 }
96 96
 
97
-// IgnoreConflicts specifies whether to ignore conflicts while updating
98
-// the mapping (default: false).
99
-func (s *IndicesPutMappingService) IgnoreConflicts(ignoreConflicts bool) *IndicesPutMappingService {
100
-	s.ignoreConflicts = &ignoreConflicts
97
+// UpdateAllTypes, if true, indicates that all fields that span multiple indices
98
+// should be updated (default: false).
99
+func (s *IndicesPutMappingService) UpdateAllTypes(updateAllTypes bool) *IndicesPutMappingService {
100
+	s.updateAllTypes = &updateAllTypes
101 101
 	return s
102 102
 }
103 103
 
@@ -153,8 +153,8 @@ func (s *IndicesPutMappingService) buildURL() (string, url.Values, error) {
153 153
 	if s.expandWildcards != "" {
154 154
 		params.Set("expand_wildcards", s.expandWildcards)
155 155
 	}
156
-	if s.ignoreConflicts != nil {
157
-		params.Set("ignore_conflicts", fmt.Sprintf("%v", *s.ignoreConflicts))
156
+	if s.updateAllTypes != nil {
157
+		params.Set("update_all_types", fmt.Sprintf("%v", *s.updateAllTypes))
158 158
 	}
159 159
 	if s.timeout != "" {
160 160
 		params.Set("timeout", s.timeout)

+ 124
- 1
reindex.go View File

@@ -280,6 +280,48 @@ func (s *ReindexService) Do(ctx context.Context) (*BulkIndexByScrollResponse, er
280 280
 	return ret, nil
281 281
 }
282 282
 
283
+// DoAsync executes the reindexing operation asynchronously by starting a new task.
284
+// Callers need to use the Task Management API to watch the outcome of the reindexing
285
+// operation.
286
+func (s *ReindexService) DoAsync(ctx context.Context) (*StartTaskResult, error) {
287
+	// Check pre-conditions
288
+	if err := s.Validate(); err != nil {
289
+		return nil, err
290
+	}
291
+
292
+	// DoAsync only makes sense with WaitForCompletion set to true
293
+	if s.waitForCompletion != nil && *s.waitForCompletion {
294
+		return nil, fmt.Errorf("cannot start a task with WaitForCompletion set to true")
295
+	}
296
+	f := false
297
+	s.waitForCompletion = &f
298
+
299
+	// Get URL for request
300
+	path, params, err := s.buildURL()
301
+	if err != nil {
302
+		return nil, err
303
+	}
304
+
305
+	// Setup HTTP request body
306
+	body, err := s.getBody()
307
+	if err != nil {
308
+		return nil, err
309
+	}
310
+
311
+	// Get HTTP response
312
+	res, err := s.client.PerformRequest(ctx, "POST", path, params, body)
313
+	if err != nil {
314
+		return nil, err
315
+	}
316
+
317
+	// Return operation response
318
+	ret := new(StartTaskResult)
319
+	if err := s.client.decoder.Decode(res.Body, ret); err != nil {
320
+		return nil, err
321
+	}
322
+	return ret, nil
323
+}
324
+
283 325
 // -- Source of Reindex --
284 326
 
285 327
 // ReindexSource specifies the source of a Reindex process.
@@ -295,6 +337,7 @@ type ReindexSource struct {
295 337
 	sorts        []SortInfo
296 338
 	sorters      []Sorter
297 339
 	searchSource *SearchSource
340
+	remoteInfo   *ReindexRemoteInfo
298 341
 }
299 342
 
300 343
 // NewReindexSource creates a new ReindexSource.
@@ -359,12 +402,18 @@ func (s *ReindexSource) SortWithInfo(info SortInfo) *ReindexSource {
359 402
 	return s
360 403
 }
361 404
 
362
-// SortBy	adds a sort order.
405
+// SortBy adds a sort order.
363 406
 func (s *ReindexSource) SortBy(sorter ...Sorter) *ReindexSource {
364 407
 	s.sorters = append(s.sorters, sorter...)
365 408
 	return s
366 409
 }
367 410
 
411
+// RemoteInfo sets up reindexing from a remote cluster.
412
+func (s *ReindexSource) RemoteInfo(ri *ReindexRemoteInfo) *ReindexSource {
413
+	s.remoteInfo = ri
414
+	return s
415
+}
416
+
368 417
 // Source returns a serializable JSON request for the request.
369 418
 func (r *ReindexSource) Source() (interface{}, error) {
370 419
 	source := make(map[string]interface{})
@@ -415,6 +464,14 @@ func (r *ReindexSource) Source() (interface{}, error) {
415 464
 		source["scroll"] = r.scroll
416 465
 	}
417 466
 
467
+	if r.remoteInfo != nil {
468
+		src, err := r.remoteInfo.Source()
469
+		if err != nil {
470
+			return nil, err
471
+		}
472
+		source["remote"] = src
473
+	}
474
+
418 475
 	if len(r.sorters) > 0 {
419 476
 		var sortarr []interface{}
420 477
 		for _, sorter := range r.sorters {
@@ -440,6 +497,72 @@ func (r *ReindexSource) Source() (interface{}, error) {
440 497
 	return source, nil
441 498
 }
442 499
 
500
+// ReindexRemoteInfo contains information for reindexing from a remote cluster.
501
+type ReindexRemoteInfo struct {
502
+	host           string
503
+	username       string
504
+	password       string
505
+	socketTimeout  string // e.g. "1m" or "30s"
506
+	connectTimeout string // e.g. "1m" or "30s"
507
+}
508
+
509
+// NewReindexRemoteInfo creates a new ReindexRemoteInfo.
510
+func NewReindexRemoteInfo() *ReindexRemoteInfo {
511
+	return &ReindexRemoteInfo{}
512
+}
513
+
514
+// Host sets the host information of the remote cluster.
515
+// It must be of the form "http(s)://<hostname>:<port>"
516
+func (ri *ReindexRemoteInfo) Host(host string) *ReindexRemoteInfo {
517
+	ri.host = host
518
+	return ri
519
+}
520
+
521
+// Username sets the username to authenticate with the remote cluster.
522
+func (ri *ReindexRemoteInfo) Username(username string) *ReindexRemoteInfo {
523
+	ri.username = username
524
+	return ri
525
+}
526
+
527
+// Password sets the password to authenticate with the remote cluster.
528
+func (ri *ReindexRemoteInfo) Password(password string) *ReindexRemoteInfo {
529
+	ri.password = password
530
+	return ri
531
+}
532
+
533
+// SocketTimeout sets the socket timeout to connect with the remote cluster.
534
+// Use ES compatible values like e.g. "30s" or "1m".
535
+func (ri *ReindexRemoteInfo) SocketTimeout(timeout string) *ReindexRemoteInfo {
536
+	ri.socketTimeout = timeout
537
+	return ri
538
+}
539
+
540
+// ConnectTimeout sets the connection timeout to connect with the remote cluster.
541
+// Use ES compatible values like e.g. "30s" or "1m".
542
+func (ri *ReindexRemoteInfo) ConnectTimeout(timeout string) *ReindexRemoteInfo {
543
+	ri.connectTimeout = timeout
544
+	return ri
545
+}
546
+
547
+// Source returns the serializable JSON data for the request.
548
+func (ri *ReindexRemoteInfo) Source() (interface{}, error) {
549
+	res := make(map[string]interface{})
550
+	res["host"] = ri.host
551
+	if len(ri.username) > 0 {
552
+		res["username"] = ri.username
553
+	}
554
+	if len(ri.password) > 0 {
555
+		res["password"] = ri.password
556
+	}
557
+	if len(ri.socketTimeout) > 0 {
558
+		res["socket_timeout"] = ri.socketTimeout
559
+	}
560
+	if len(ri.connectTimeout) > 0 {
561
+		res["connect_timeout"] = ri.connectTimeout
562
+	}
563
+	return res, nil
564
+}
565
+
443 566
 // -source Destination of Reindex --
444 567
 
445 568
 // ReindexDestination is the destination of a Reindex API call.

+ 110
- 0
reindex_test.go View File

@@ -82,6 +82,31 @@ func TestReindexSourceWithSourceAndDestinationAndVersionType(t *testing.T) {
82 82
 	}
83 83
 }
84 84
 
85
+func TestReindexSourceWithSourceAndRemoteAndDestination(t *testing.T) {
86
+	client := setupTestClient(t)
87
+	src := NewReindexSource().Index("twitter").RemoteInfo(
88
+		NewReindexRemoteInfo().Host("http://otherhost:9200").
89
+			Username("alice").
90
+			Password("secret").
91
+			ConnectTimeout("10s").
92
+			SocketTimeout("1m"),
93
+	)
94
+	dst := NewReindexDestination().Index("new_twitter")
95
+	out, err := client.Reindex().Source(src).Destination(dst).getBody()
96
+	if err != nil {
97
+		t.Fatal(err)
98
+	}
99
+	b, err := json.Marshal(out)
100
+	if err != nil {
101
+		t.Fatal(err)
102
+	}
103
+	got := string(b)
104
+	want := `{"dest":{"index":"new_twitter"},"source":{"index":"twitter","remote":{"connect_timeout":"10s","host":"http://otherhost:9200","password":"secret","socket_timeout":"1m","username":"alice"}}}`
105
+	if got != want {
106
+		t.Fatalf("\ngot  %s\nwant %s", got, want)
107
+	}
108
+}
109
+
85 110
 func TestReindexSourceWithSourceAndDestinationAndOpType(t *testing.T) {
86 111
 	client := setupTestClient(t)
87 112
 	src := NewReindexSource().Index("twitter")
@@ -289,3 +314,88 @@ func TestReindex(t *testing.T) {
289 314
 		t.Fatalf("expected %d documents; got: %d", sourceCount, targetCount)
290 315
 	}
291 316
 }
317
+
318
+func TestReindexAsync(t *testing.T) {
319
+	client := setupTestClientAndCreateIndexAndAddDocs(t) //, SetTraceLog(log.New(os.Stdout, "", 0)))
320
+	esversion, err := client.ElasticsearchVersion(DefaultURL)
321
+	if err != nil {
322
+		t.Fatal(err)
323
+	}
324
+	if esversion < "2.3.0" {
325
+		t.Skipf("Elasticsearch %v does not support Reindex API yet", esversion)
326
+	}
327
+
328
+	sourceCount, err := client.Count(testIndexName).Do(context.TODO())
329
+	if err != nil {
330
+		t.Fatal(err)
331
+	}
332
+	if sourceCount <= 0 {
333
+		t.Fatalf("expected more than %d documents; got: %d", 0, sourceCount)
334
+	}
335
+
336
+	targetCount, err := client.Count(testIndexName2).Do(context.TODO())
337
+	if err != nil {
338
+		t.Fatal(err)
339
+	}
340
+	if targetCount != 0 {
341
+		t.Fatalf("expected %d documents; got: %d", 0, targetCount)
342
+	}
343
+
344
+	// Simple copying
345
+	src := NewReindexSource().Index(testIndexName)
346
+	dst := NewReindexDestination().Index(testIndexName2)
347
+	res, err := client.Reindex().Source(src).Destination(dst).DoAsync(context.TODO())
348
+	if err != nil {
349
+		t.Fatal(err)
350
+	}
351
+	if res == nil {
352
+		t.Fatal("expected result != nil")
353
+	}
354
+	if res.TaskId == "" {
355
+		t.Errorf("expected a task id, got %+v", res)
356
+	}
357
+
358
+	tasksGetTask := client.TasksGetTask()
359
+	taskStatus, err := tasksGetTask.TaskId(res.TaskId).Do(context.TODO())
360
+	if err != nil {
361
+		t.Fatal(err)
362
+	}
363
+	if taskStatus == nil {
364
+		t.Fatal("expected task status result != nil")
365
+	}
366
+}
367
+
368
+func TestReindexWithWaitForCompletionTrueCannotBeStarted(t *testing.T) {
369
+	client := setupTestClientAndCreateIndexAndAddDocs(t)
370
+	esversion, err := client.ElasticsearchVersion(DefaultURL)
371
+	if err != nil {
372
+		t.Fatal(err)
373
+	}
374
+	if esversion < "2.3.0" {
375
+		t.Skipf("Elasticsearch %v does not support Reindex API yet", esversion)
376
+	}
377
+
378
+	sourceCount, err := client.Count(testIndexName).Do(context.TODO())
379
+	if err != nil {
380
+		t.Fatal(err)
381
+	}
382
+	if sourceCount <= 0 {
383
+		t.Fatalf("expected more than %d documents; got: %d", 0, sourceCount)
384
+	}
385
+
386
+	targetCount, err := client.Count(testIndexName2).Do(context.TODO())
387
+	if err != nil {
388
+		t.Fatal(err)
389
+	}
390
+	if targetCount != 0 {
391
+		t.Fatalf("expected %d documents; got: %d", 0, targetCount)
392
+	}
393
+
394
+	// DoAsync should fail when WaitForCompletion is true
395
+	src := NewReindexSource().Index(testIndexName)
396
+	dst := NewReindexDestination().Index(testIndexName2)
397
+	_, err = client.Reindex().Source(src).Destination(dst).WaitForCompletion(true).DoAsync(context.TODO())
398
+	if err == nil {
399
+		t.Fatal("error should have been returned")
400
+	}
401
+}

+ 3
- 3
retry_test.go View File

@@ -21,14 +21,14 @@ func TestRetry(t *testing.T) {
21 21
 	// This function is successfull on "successOn" calls.
22 22
 	f := func() error {
23 23
 		i++
24
-		t.Logf("function is called %d. time\n", i)
24
+		// t.Logf("function is called %d. time\n", i)
25 25
 
26 26
 		if i == successOn {
27
-			t.Log("OK")
27
+			// t.Log("OK")
28 28
 			return nil
29 29
 		}
30 30
 
31
-		t.Log("error")
31
+		// t.Log("error")
32 32
 		return errors.New("error")
33 33
 	}
34 34
 

+ 12
- 0
search_aggs_bucket_date_range.go View File

@@ -192,20 +192,32 @@ func (a *DateRangeAggregation) Source() (interface{}, error) {
192 192
 			switch from := ent.From.(type) {
193 193
 			case int, int16, int32, int64, float32, float64:
194 194
 				r["from"] = from
195
+			case *int, *int16, *int32, *int64, *float32, *float64:
196
+				r["from"] = from
195 197
 			case time.Time:
196 198
 				r["from"] = from.Format(time.RFC3339)
199
+			case *time.Time:
200
+				r["from"] = from.Format(time.RFC3339)
197 201
 			case string:
198 202
 				r["from"] = from
203
+			case *string:
204
+				r["from"] = from
199 205
 			}
200 206
 		}
201 207
 		if ent.To != nil {
202 208
 			switch to := ent.To.(type) {
203 209
 			case int, int16, int32, int64, float32, float64:
204 210
 				r["to"] = to
211
+			case *int, *int16, *int32, *int64, *float32, *float64:
212
+				r["to"] = to
205 213
 			case time.Time:
206 214
 				r["to"] = to.Format(time.RFC3339)
215
+			case *time.Time:
216
+				r["to"] = to.Format(time.RFC3339)
207 217
 			case string:
208 218
 				r["to"] = to
219
+			case *string:
220
+				r["to"] = to
209 221
 			}
210 222
 		}
211 223
 		ranges = append(ranges, r)

+ 25
- 0
search_aggs_bucket_date_range_test.go View File

@@ -29,6 +29,31 @@ func TestDateRangeAggregation(t *testing.T) {
29 29
 	}
30 30
 }
31 31
 
32
+func TestDateRangeAggregationWithPointers(t *testing.T) {
33
+	d1 := "2012-12-31"
34
+	d2 := "2013-01-01"
35
+	d3 := "2013-12-31"
36
+	d4 := "2014-01-01"
37
+
38
+	agg := NewDateRangeAggregation().Field("created_at")
39
+	agg = agg.AddRange(nil, &d1)
40
+	agg = agg.AddRange(d2, &d3)
41
+	agg = agg.AddRange(d4, nil)
42
+	src, err := agg.Source()
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+	data, err := json.Marshal(src)
47
+	if err != nil {
48
+		t.Fatalf("marshaling to JSON failed: %v", err)
49
+	}
50
+	got := string(data)
51
+	expected := `{"date_range":{"field":"created_at","ranges":[{"to":"2012-12-31"},{"from":"2013-01-01","to":"2013-12-31"},{"from":"2014-01-01"}]}}`
52
+	if got != expected {
53
+		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
54
+	}
55
+}
56
+
32 57
 func TestDateRangeAggregationWithUnbounded(t *testing.T) {
33 58
 	agg := NewDateRangeAggregation().Field("created_at").
34 59
 		AddUnboundedFrom("2012-12-31").

+ 4
- 0
search_aggs_bucket_geo_distance.go View File

@@ -156,6 +156,8 @@ func (a *GeoDistanceAggregation) Source() (interface{}, error) {
156 156
 				r["from"] = from
157 157
 			case string:
158 158
 				r["from"] = from
159
+			case *string:
160
+				r["from"] = from
159 161
 			}
160 162
 		}
161 163
 		if ent.To != nil {
@@ -166,6 +168,8 @@ func (a *GeoDistanceAggregation) Source() (interface{}, error) {
166 168
 				r["to"] = to
167 169
 			case string:
168 170
 				r["to"] = to
171
+			case *string:
172
+				r["to"] = to
169 173
 			}
170 174
 		}
171 175
 		ranges = append(ranges, r)

+ 22
- 0
search_aggs_bucket_geo_distance_test.go View File

@@ -29,6 +29,28 @@ func TestGeoDistanceAggregation(t *testing.T) {
29 29
 	}
30 30
 }
31 31
 
32
+func TestGeoDistanceAggregationWithPointers(t *testing.T) {
33
+	hundred := 100
34
+	threeHundred := 300
35
+	agg := NewGeoDistanceAggregation().Field("location").Point("52.3760, 4.894")
36
+	agg = agg.AddRange(nil, &hundred)
37
+	agg = agg.AddRange(hundred, &threeHundred)
38
+	agg = agg.AddRange(threeHundred, nil)
39
+	src, err := agg.Source()
40
+	if err != nil {
41
+		t.Fatal(err)
42
+	}
43
+	data, err := json.Marshal(src)
44
+	if err != nil {
45
+		t.Fatalf("marshaling to JSON failed: %v", err)
46
+	}
47
+	got := string(data)
48
+	expected := `{"geo_distance":{"field":"location","origin":"52.3760, 4.894","ranges":[{"to":100},{"from":100,"to":300},{"from":300}]}}`
49
+	if got != expected {
50
+		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
51
+	}
52
+}
53
+
32 54
 func TestGeoDistanceAggregationWithUnbounded(t *testing.T) {
33 55
 	agg := NewGeoDistanceAggregation().Field("location").Point("52.3760, 4.894")
34 56
 	agg = agg.AddUnboundedFrom(100)

+ 12
- 0
search_aggs_bucket_range.go View File

@@ -191,20 +191,32 @@ func (a *RangeAggregation) Source() (interface{}, error) {
191 191
 			switch from := ent.From.(type) {
192 192
 			case int, int16, int32, int64, float32, float64:
193 193
 				r["from"] = from
194
+			case *int, *int16, *int32, *int64, *float32, *float64:
195
+				r["from"] = from
194 196
 			case time.Time:
195 197
 				r["from"] = from.Format(time.RFC3339)
198
+			case *time.Time:
199
+				r["from"] = from.Format(time.RFC3339)
196 200
 			case string:
197 201
 				r["from"] = from
202
+			case *string:
203
+				r["from"] = from
198 204
 			}
199 205
 		}
200 206
 		if ent.To != nil {
201 207
 			switch to := ent.To.(type) {
202 208
 			case int, int16, int32, int64, float32, float64:
203 209
 				r["to"] = to
210
+			case *int, *int16, *int32, *int64, *float32, *float64:
211
+				r["to"] = to
204 212
 			case time.Time:
205 213
 				r["to"] = to.Format(time.RFC3339)
214
+			case *time.Time:
215
+				r["to"] = to.Format(time.RFC3339)
206 216
 			case string:
207 217
 				r["to"] = to
218
+			case *string:
219
+				r["to"] = to
208 220
 			}
209 221
 		}
210 222
 		ranges = append(ranges, r)

+ 22
- 0
search_aggs_bucket_range_test.go View File

@@ -29,6 +29,28 @@ func TestRangeAggregation(t *testing.T) {
29 29
 	}
30 30
 }
31 31
 
32
+func TestRangeAggregationWithPointers(t *testing.T) {
33
+	fifty := 50
34
+	hundred := 100
35
+	agg := NewRangeAggregation().Field("price")
36
+	agg = agg.AddRange(nil, &fifty)
37
+	agg = agg.AddRange(fifty, &hundred)
38
+	agg = agg.AddRange(hundred, nil)
39
+	src, err := agg.Source()
40
+	if err != nil {
41
+		t.Fatal(err)
42
+	}
43
+	data, err := json.Marshal(src)
44
+	if err != nil {
45
+		t.Fatalf("marshaling to JSON failed: %v", err)
46
+	}
47
+	got := string(data)
48
+	expected := `{"range":{"field":"price","ranges":[{"to":50},{"from":50,"to":100},{"from":100}]}}`
49
+	if got != expected {
50
+		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
51
+	}
52
+}
53
+
32 54
 func TestRangeAggregationWithUnbounded(t *testing.T) {
33 55
 	agg := NewRangeAggregation().Field("field_name").
34 56
 		AddUnboundedFrom(50).

+ 1
- 1
search_aggs_metrics_top_hits_test.go View File

@@ -24,7 +24,7 @@ func TestTopHitsAggregation(t *testing.T) {
24 24
 		t.Fatalf("marshaling to JSON failed: %v", err)
25 25
 	}
26 26
 	got := string(data)
27
-	expected := `{"top_hits":{"_source":{"excludes":[],"includes":["title"]},"size":1,"sort":[{"last_activity_date":{"order":"desc"}}]}}`
27
+	expected := `{"top_hits":{"_source":{"includes":["title"]},"size":1,"sort":[{"last_activity_date":{"order":"desc"}}]}}`
28 28
 	if got != expected {
29 29
 		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
30 30
 	}

+ 2
- 1
search_queries_terms.go View File

@@ -20,7 +20,8 @@ type TermsQuery struct {
20 20
 // NewTermsQuery creates and initializes a new TermsQuery.
21 21
 func NewTermsQuery(name string, values ...interface{}) *TermsQuery {
22 22
 	q := &TermsQuery{
23
-		name: name,
23
+		name:   name,
24
+		values: make([]interface{}, 0),
24 25
 	}
25 26
 	if len(values) > 0 {
26 27
 		q.values = append(q.values, values...)

+ 18
- 0
search_queries_terms_test.go View File

@@ -26,6 +26,24 @@ func TestTermsQuery(t *testing.T) {
26 26
 	}
27 27
 }
28 28
 
29
+func TestTermsQueryWithEmptyArray(t *testing.T) {
30
+	included := make([]interface{}, 0)
31
+	q := NewTermsQuery("tags", included...)
32
+	src, err := q.Source()
33
+	if err != nil {
34
+		t.Fatal(err)
35
+	}
36
+	data, err := json.Marshal(src)
37
+	if err != nil {
38
+		t.Fatalf("marshaling to JSON failed: %v", err)
39
+	}
40
+	got := string(data)
41
+	expected := `{"terms":{"tags":[]}}`
42
+	if got != expected {
43
+		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
44
+	}
45
+}
46
+
29 47
 func TestTermsQueryWithTermsLookup(t *testing.T) {
30 48
 	q := NewTermsQuery("user").
31 49
 		TermsLookup(NewTermsLookup().Index("users").Type("user").Id("2").Path("followers"))

+ 1
- 1
suggester_term.go View File

@@ -214,7 +214,7 @@ func (q *TermSuggester) Source(includeName bool) (interface{}, error) {
214 214
 		suggester["max_term_freq"] = *q.maxTermFreq
215 215
 	}
216 216
 	if q.prefixLength != nil {
217
-		suggester["prefix_len"] = *q.prefixLength
217
+		suggester["prefix_length"] = *q.prefixLength
218 218
 	}
219 219
 	if q.minWordLength != nil {
220 220
 		suggester["min_word_len"] = *q.minWordLength

+ 20
- 0
suggester_term_test.go View File

@@ -27,3 +27,23 @@ func TestTermSuggesterSource(t *testing.T) {
27 27
 		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
28 28
 	}
29 29
 }
30
+
31
+func TestTermSuggesterWithPrefixLengthSource(t *testing.T) {
32
+	s := NewTermSuggester("name").
33
+		Text("n").
34
+		Field("suggest").
35
+		PrefixLength(0)
36
+	src, err := s.Source(true)
37
+	if err != nil {
38
+		t.Fatal(err)
39
+	}
40
+	data, err := json.Marshal(src)
41
+	if err != nil {
42
+		t.Fatalf("marshaling to JSON failed: %v", err)
43
+	}
44
+	got := string(data)
45
+	expected := `{"name":{"text":"n","term":{"field":"suggest","prefix_length":0}}}`
46
+	if got != expected {
47
+		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
48
+	}
49
+}

+ 104
- 0
tasks_get_task.go View File

@@ -0,0 +1,104 @@
1
+package elastic
2
+
3
+import (
4
+	"context"
5
+	"fmt"
6
+	"net/url"
7
+
8
+	"gopkg.in/olivere/elastic.v5/uritemplates"
9
+)
10
+
11
+// TasksGetTaskService retrieves the state of a task in the cluster. It is part of the Task Management API
12
+// documented at http://www.elastic.co/guide/en/elasticsearch/reference/5.2/tasks-list.html.
13
+//
14
+// It is supported as of Elasticsearch 2.3.0.
15
+type TasksGetTaskService struct {
16
+	client            *Client
17
+	pretty            bool
18
+	taskId            string
19
+	waitForCompletion *bool
20
+}
21
+
22
+// NewTasksGetTaskService creates a new TasksGetTaskService.
23
+func NewTasksGetTaskService(client *Client) *TasksGetTaskService {
24
+	return &TasksGetTaskService{
25
+		client: client,
26
+	}
27
+}
28
+
29
+// TaskId indicates to return the task with specified id.
30
+func (s *TasksGetTaskService) TaskId(taskId string) *TasksGetTaskService {
31
+	s.taskId = taskId
32
+	return s
33
+}
34
+
35
+// WaitForCompletion indicates whether to wait for the matching tasks
36
+// to complete (default: false).
37
+func (s *TasksGetTaskService) WaitForCompletion(waitForCompletion bool) *TasksGetTaskService {
38
+	s.waitForCompletion = &waitForCompletion
39
+	return s
40
+}
41
+
42
+// Pretty indicates that the JSON response be indented and human readable.
43
+func (s *TasksGetTaskService) Pretty(pretty bool) *TasksGetTaskService {
44
+	s.pretty = pretty
45
+	return s
46
+}
47
+
48
+// buildURL builds the URL for the operation.
49
+func (s *TasksGetTaskService) buildURL() (string, url.Values, error) {
50
+	// Build URL
51
+	path, err := uritemplates.Expand("/_tasks/{task_id}", map[string]string{
52
+		"task_id": s.taskId,
53
+	})
54
+	if err != nil {
55
+		return "", url.Values{}, err
56
+	}
57
+
58
+	// Add query string parameters
59
+	params := url.Values{}
60
+	if s.pretty {
61
+		params.Set("pretty", "1")
62
+	}
63
+	if s.waitForCompletion != nil {
64
+		params.Set("wait_for_completion", fmt.Sprintf("%v", *s.waitForCompletion))
65
+	}
66
+	return path, params, nil
67
+}
68
+
69
+// Validate checks if the operation is valid.
70
+func (s *TasksGetTaskService) Validate() error {
71
+	return nil
72
+}
73
+
74
+// Do executes the operation.
75
+func (s *TasksGetTaskService) Do(ctx context.Context) (*TasksGetTaskResponse, error) {
76
+	// Check pre-conditions
77
+	if err := s.Validate(); err != nil {
78
+		return nil, err
79
+	}
80
+
81
+	// Get URL for request
82
+	path, params, err := s.buildURL()
83
+	if err != nil {
84
+		return nil, err
85
+	}
86
+
87
+	// Get HTTP response
88
+	res, err := s.client.PerformRequest(ctx, "GET", path, params, nil)
89
+	if err != nil {
90
+		return nil, err
91
+	}
92
+
93
+	// Return operation response
94
+	ret := new(TasksGetTaskResponse)
95
+	if err := s.client.decoder.Decode(res.Body, ret); err != nil {
96
+		return nil, err
97
+	}
98
+	return ret, nil
99
+}
100
+
101
+type TasksGetTaskResponse struct {
102
+	Completed bool      `json:"completed"`
103
+	Task      *TaskInfo `json:"task,omitempty"`
104
+}

+ 43
- 0
tasks_get_task_test.go View File

@@ -0,0 +1,43 @@
1
+// Copyright 2012-present Oliver Eilhard. All rights reserved.
2
+// Use of this source code is governed by a MIT-license.
3
+// See http://olivere.mit-license.org/license.txt for details.
4
+
5
+package elastic
6
+
7
+import (
8
+	"testing"
9
+)
10
+
11
+func TestTasksGetTaskBuildURL(t *testing.T) {
12
+	client := setupTestClient(t)
13
+
14
+	// Get specific task
15
+	got, _, err := client.TasksGetTask().TaskId("123").buildURL()
16
+	if err != nil {
17
+		t.Fatal(err)
18
+	}
19
+	want := "/_tasks/123"
20
+	if got != want {
21
+		t.Errorf("want %q; got %q", want, got)
22
+	}
23
+}
24
+
25
+/*
26
+func TestTasksGetTask(t *testing.T) {
27
+	client := setupTestClientAndCreateIndexAndAddDocs(t)
28
+	esversion, err := client.ElasticsearchVersion(DefaultURL)
29
+	if err != nil {
30
+		t.Fatal(err)
31
+	}
32
+	if esversion < "2.3.0" {
33
+		t.Skipf("Elasticsearch %v does not support Tasks Management API yet", esversion)
34
+	}
35
+	res, err := client.TasksGetTask().TaskId("123").Do(context.TODO())
36
+	if err != nil {
37
+		t.Fatal(err)
38
+	}
39
+	if res == nil {
40
+		t.Fatal("response is nil")
41
+	}
42
+}
43
+*/

+ 9
- 2
tasks_list.go View File

@@ -204,11 +204,18 @@ type TaskInfo struct {
204 204
 	Id                 int64       `json:"id"` // the task id
205 205
 	Type               string      `json:"type"`
206 206
 	Action             string      `json:"action"`
207
-	Status             interface{} `json:"status"`
208
-	Description        interface{} `json:"description"`
207
+	Status             interface{} `json:"status"`      // has separate implementations of Task.Status in Java for reindexing, replication, and "RawTaskStatus"
208
+	Description        interface{} `json:"description"` // same as Status
209 209
 	StartTime          string      `json:"start_time"`
210 210
 	StartTimeInMillis  int64       `json:"start_time_in_millis"`
211 211
 	RunningTime        string      `json:"running_time"`
212 212
 	RunningTimeInNanos int64       `json:"running_time_in_nanos"`
213
+	Cancellable        bool        `json:"cancellable"`
213 214
 	ParentTaskId       string      `json:"parent_task_id"` // like "YxJnVYjwSBm_AUbzddTajQ:12356"
214 215
 }
216
+
217
+// StartTaskResult is used in cases where a task gets started asynchronously and
218
+// the operation simply returnes a TaskID to watch for via the Task Management API.
219
+type StartTaskResult struct {
220
+	TaskId string `json:"task"`
221
+}

+ 25
- 0
update.go View File

@@ -25,6 +25,7 @@ type UpdateService struct {
25 25
 	parent              string
26 26
 	script              *Script
27 27
 	fields              []string
28
+	fsc                 *FetchSourceContext
28 29
 	version             *int64
29 30
 	versionType         string
30 31
 	retryOnConflict     *int
@@ -172,6 +173,23 @@ func (b *UpdateService) Pretty(pretty bool) *UpdateService {
172 173
 	return b
173 174
 }
174 175
 
176
+// FetchSource asks Elasticsearch to return the updated _source in the response.
177
+func (s *UpdateService) FetchSource(fetchSource bool) *UpdateService {
178
+	if s.fsc == nil {
179
+		s.fsc = NewFetchSourceContext(fetchSource)
180
+	} else {
181
+		s.fsc.SetFetchSource(fetchSource)
182
+	}
183
+	return s
184
+}
185
+
186
+// FetchSourceContext indicates that _source should be returned in the response,
187
+// allowing wildcard patterns to be defined via FetchSourceContext.
188
+func (s *UpdateService) FetchSourceContext(fetchSourceContext *FetchSourceContext) *UpdateService {
189
+	s.fsc = fetchSourceContext
190
+	return s
191
+}
192
+
175 193
 // url returns the URL part of the document request.
176 194
 func (b *UpdateService) url() (string, url.Values, error) {
177 195
 	// Build url
@@ -250,6 +268,13 @@ func (b *UpdateService) body() (interface{}, error) {
250 268
 	if b.detectNoop != nil {
251 269
 		source["detect_noop"] = *b.detectNoop
252 270
 	}
271
+	if b.fsc != nil {
272
+		src, err := b.fsc.Source()
273
+		if err != nil {
274
+			return nil, err
275
+		}
276
+		source["_source"] = src
277
+	}
253 278
 
254 279
 	return source, nil
255 280
 }

+ 68
- 0
update_test.go View File

@@ -5,6 +5,7 @@
5 5
 package elastic
6 6
 
7 7
 import (
8
+	"context"
8 9
 	"encoding/json"
9 10
 	"net/url"
10 11
 	"testing"
@@ -231,3 +232,70 @@ func TestUpdateViaDocAndUpsert(t *testing.T) {
231 232
 		t.Errorf("expected\n%s\ngot:\n%s", expected, got)
232 233
 	}
233 234
 }
235
+
236
+func TestUpdateViaDocAndUpsertAndFetchSource(t *testing.T) {
237
+	client := setupTestClient(t)
238
+	update := client.Update().
239
+		Index("test").Type("type1").Id("1").
240
+		Doc(map[string]interface{}{"name": "new_name"}).
241
+		DocAsUpsert(true).
242
+		Timeout("1s").
243
+		Refresh("true").
244
+		FetchSource(true)
245
+	path, params, err := update.url()
246
+	if err != nil {
247
+		t.Fatalf("expected to return URL, got: %v", err)
248
+	}
249
+	expectedPath := `/test/type1/1/_update`
250
+	if expectedPath != path {
251
+		t.Errorf("expected URL path\n%s\ngot:\n%s", expectedPath, path)
252
+	}
253
+	expectedParams := url.Values{
254
+		"refresh": []string{"true"},
255
+		"timeout": []string{"1s"},
256
+	}
257
+	if expectedParams.Encode() != params.Encode() {
258
+		t.Errorf("expected URL parameters\n%s\ngot:\n%s", expectedParams.Encode(), params.Encode())
259
+	}
260
+	body, err := update.body()
261
+	if err != nil {
262
+		t.Fatalf("expected to return body, got: %v", err)
263
+	}
264
+	data, err := json.Marshal(body)
265
+	if err != nil {
266
+		t.Fatalf("expected to marshal body as JSON, got: %v", err)
267
+	}
268
+	got := string(data)
269
+	expected := `{"_source":true,"doc":{"name":"new_name"},"doc_as_upsert":true}`
270
+	if got != expected {
271
+		t.Errorf("expected\n%s\ngot:\n%s", expected, got)
272
+	}
273
+}
274
+
275
+func TestUpdateAndFetchSource(t *testing.T) {
276
+	client := setupTestClientAndCreateIndexAndAddDocs(t) // , SetTraceLog(log.New(os.Stdout, "", 0)))
277
+	res, err := client.Update().
278
+		Index(testIndexName).Type("tweet").Id("1").
279
+		Doc(map[string]interface{}{"user": "sandrae"}).
280
+		DetectNoop(true).
281
+		FetchSource(true).
282
+		Do(context.Background())
283
+	if err != nil {
284
+		t.Fatal(err)
285
+	}
286
+	if res == nil {
287
+		t.Fatal("expected response != nil")
288
+	}
289
+	if res.GetResult == nil {
290
+		t.Fatal("expected GetResult != nil")
291
+	}
292
+	data, err := json.Marshal(res.GetResult.Source)
293
+	if err != nil {
294
+		t.Fatalf("expected to marshal body as JSON, got: %v", err)
295
+	}
296
+	got := string(data)
297
+	expected := `{"user":"sandrae","message":"Welcome to Golang and Elasticsearch.","retweets":0,"created":"0001-01-01T00:00:00Z"}`
298
+	if got != expected {
299
+		t.Errorf("expected\n%s\ngot:\n%s", expected, got)
300
+	}
301
+}

Loading…
Cancel
Save