├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cluster.go ├── cluster_test.go ├── index.go ├── index_test.go ├── node.go ├── query_test.go ├── requests.go ├── requests_test.go ├── response.go ├── searcher.go └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-* 2 | elasticsearch-*.tar.gz 3 | elasticsearch-*/ 4 | 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | services: 4 | - elasticsearch 5 | 6 | script: 7 | - go test -tags=cluster -v -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Peter Bourgon, SoundCloud Ltd. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elasticsearch 2 | 3 | This is an opinionated library for ElasticSearch in Go. Its opinions are: 4 | 5 | * Builders are bad: construct queries declaratively, using nested structures 6 | * Cleverness is bad: when in doubt, be explicit and dumb 7 | 8 | [![Build Status][1]][2] 9 | 10 | [1]: https://drone.io/github.com/peterbourgon/elasticsearch/status.png 11 | [2]: https://drone.io/github.com/peterbourgon/elasticsearch/latest 12 | 13 | 14 | # Usage 15 | 16 | First, it helps to import the package with a short name (package alias). 17 | 18 | ```go 19 | import es "github.com/peterbourgon/elasticsearch" 20 | ``` 21 | 22 | Create a Cluster, which is an actively-managed handle to a set of nodes. 23 | 24 | ```go 25 | endpoints := []string{"http://host1:9200", "http://host2:9200"} 26 | pingInterval, pingTimeout := 30*time.Second, 3*time.Second 27 | c := es.NewCluster(endpoints, pingInterval, pingTimeout) 28 | ``` 29 | 30 | Construct queries declaratively, and fire them against the cluster. 31 | 32 | ```go 33 | q := es.QueryWrapper( 34 | es.TermQuery(es.TermQueryParams{ 35 | Query: &es.Wrapper{ 36 | Name: "user", 37 | Wrapped: "kimchy", 38 | }, 39 | }), 40 | ) 41 | 42 | request := &es.SearchRequest{ 43 | Params: es.SearchParams{ 44 | Indices: []string{"twitter"}, 45 | Types: []string{"tweet"}, 46 | }, 47 | Query: q, 48 | } 49 | 50 | response, err := c.Search(request) 51 | if err != nil { 52 | // Fatal 53 | } 54 | fmt.Printf("got %d hit(s)", response.HitsWrapper.Total) 55 | ``` 56 | 57 | -------------------------------------------------------------------------------- /cluster.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // A Cluster is an actively-managed collection of Nodes. Cluster implements 8 | // Searcher, so you can treat it as a single entity. Its Search method chooses 9 | // the best Node to receive the Request. 10 | type Cluster struct { 11 | nodes Nodes 12 | pingInterval time.Duration 13 | shutdown chan chan bool 14 | } 15 | 16 | // NewCluster returns a new, actively-managed Cluster, representing the 17 | // passed endpoints as Nodes. Each endpoint should be of the form 18 | // scheme://host:port, for example http://es001:9200. 19 | // 20 | // The Cluster will ping each Node on a schedule dictated by pingInterval. 21 | // Each node has pingTimeout to respond before the ping is marked as failed. 22 | // 23 | // TODO node discovery from the list of seed-nodes. 24 | func NewCluster(endpoints []string, pingInterval, pingTimeout time.Duration) *Cluster { 25 | nodes := Nodes{} 26 | for _, endpoint := range endpoints { 27 | nodes = append(nodes, NewNode(endpoint, pingTimeout)) 28 | } 29 | 30 | c := &Cluster{ 31 | nodes: nodes, 32 | pingInterval: pingInterval, 33 | shutdown: make(chan chan bool), 34 | } 35 | go c.loop() 36 | return c 37 | } 38 | 39 | // loop is the event dispatcher for a Cluster. It manages the regular pinging of 40 | // Nodes, and serves incoming requests. Because every request against the 41 | // cluster must pass through here, it cannot block. 42 | func (c *Cluster) loop() { 43 | ticker := time.Tick(c.pingInterval) 44 | for { 45 | select { 46 | case <-ticker: 47 | go c.nodes.pingAll() 48 | 49 | case q := <-c.shutdown: 50 | q <- true 51 | return 52 | } 53 | } 54 | } 55 | 56 | // Search implements the Searcher interface for a Cluster. It executes the 57 | // request against a suitable node. 58 | func (c *Cluster) Search(r SearchRequest) (response SearchResponse, err error) { 59 | err = c.Execute(r, &response) 60 | return 61 | } 62 | 63 | // MultiSearch implements the MultiSearcher interface for a Cluster. It 64 | // executes the search request against a suitable node. 65 | func (c *Cluster) MultiSearch(r MultiSearchRequest) (response MultiSearchResponse, err error) { 66 | err = c.Execute(r, &response) 67 | return 68 | } 69 | 70 | func (c *Cluster) Index(r IndexRequest) (response IndexResponse, err error) { 71 | err = c.Execute(r, &response) 72 | return 73 | } 74 | 75 | func (c *Cluster) Create(r CreateRequest) (response IndexResponse, err error) { 76 | err = c.Execute(r, &response) 77 | return 78 | } 79 | 80 | func (c *Cluster) Update(r UpdateRequest) (response IndexResponse, err error) { 81 | err = c.Execute(r, &response) 82 | return 83 | } 84 | 85 | func (c *Cluster) Delete(r DeleteRequest) (response IndexResponse, err error) { 86 | err = c.Execute(r, &response) 87 | return 88 | } 89 | 90 | func (c *Cluster) Bulk(r BulkRequest) (response BulkResponse, err error) { 91 | err = c.Execute(r, &response) 92 | return 93 | } 94 | 95 | // Executes the request against a suitable node and decodes server's reply into 96 | // response. 97 | func (c *Cluster) Execute(f Fireable, response interface{}) error { 98 | node, err := c.nodes.getBest() 99 | if err != nil { 100 | return err 101 | } 102 | 103 | return node.Execute(f, response) 104 | } 105 | 106 | // Shutdown terminates the Cluster's event dispatcher. 107 | func (c *Cluster) Shutdown() { 108 | q := make(chan bool) 109 | c.shutdown <- q 110 | <-q 111 | } 112 | -------------------------------------------------------------------------------- /cluster_test.go: -------------------------------------------------------------------------------- 1 | // +build cluster 2 | 3 | // This file is only built and run if you specify 4 | // -tags=cluster as part of the 'go test' invocation. 5 | // http://golang.org/pkg/go/build/#Build_Constraints 6 | 7 | package elasticsearch_test 8 | 9 | import ( 10 | "bytes" 11 | "encoding/json" 12 | "fmt" 13 | es "github.com/peterbourgon/elasticsearch" 14 | "io/ioutil" 15 | "net/http" 16 | "testing" 17 | "time" 18 | ) 19 | 20 | func init() { 21 | waitForCluster(15 * time.Second) 22 | } 23 | 24 | // Just tests es.Cluster internals; doesn't make any real connection. 25 | func TestClusterShutdown(t *testing.T) { 26 | endpoints := []string{"http://host1:9200", "http://host2:9200"} 27 | pingInterval, pingTimeout := 30*time.Second, 3*time.Second 28 | c := es.NewCluster(endpoints, pingInterval, pingTimeout) 29 | 30 | e := make(chan error) 31 | go func() { 32 | c.Shutdown() 33 | e <- nil 34 | }() 35 | go func() { 36 | <-time.After(1 * time.Second) 37 | e <- fmt.Errorf("timeout") 38 | }() 39 | 40 | if err := <-e; err != nil { 41 | t.Fatalf("%s", err) 42 | } 43 | } 44 | 45 | func TestClusterIndex(t *testing.T) { 46 | c := newCluster(t, []string{"twitter"}, nil) 47 | defer c.Shutdown() 48 | defer deleteIndices(t, []string{"twitter"}) 49 | 50 | response, err := c.Index(es.IndexRequest{ 51 | es.IndexParams{ 52 | Index: "twitter", 53 | Type: "tweet", 54 | Id: "1", 55 | Refresh: "true", 56 | }, 57 | map[string]interface{}{ 58 | "name": "John", 59 | }, 60 | }) 61 | 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | if response.Error != "" { 67 | t.Error(response.Error) 68 | } 69 | 70 | if expected, got := 1, response.Version; expected != got { 71 | t.Errorf("expected version to be %d; got %d", expected, got) 72 | } 73 | } 74 | 75 | func TestClusterCreate(t *testing.T) { 76 | c := newCluster(t, []string{"twitter"}, nil) 77 | defer c.Shutdown() 78 | defer deleteIndices(t, []string{"twitter"}) 79 | 80 | response, err := c.Create(es.CreateRequest{ 81 | es.IndexParams{ 82 | Index: "twitter", 83 | Type: "tweet", 84 | Id: "1", 85 | Refresh: "true", 86 | }, 87 | map[string]interface{}{ 88 | "name": "John", 89 | }, 90 | }) 91 | 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if response.Error != "" { 97 | t.Error(response.Error) 98 | } 99 | 100 | if expected, got := 1, response.Version; expected != got { 101 | t.Errorf("expected version to be %d; got %d", expected, got) 102 | } 103 | } 104 | 105 | func TestClusterUpdate(t *testing.T) { 106 | c := newCluster(t, []string{"twitter"}, map[string]interface{}{ 107 | "/twitter/tweet/1": map[string]string{ 108 | "name": "John", 109 | }, 110 | }) 111 | defer c.Shutdown() 112 | defer deleteIndices(t, []string{"twitter"}) 113 | 114 | response, err := c.Update(es.UpdateRequest{ 115 | es.IndexParams{ 116 | Index: "twitter", 117 | Type: "tweet", 118 | Id: "1", 119 | Refresh: "true", 120 | }, 121 | map[string]interface{}{ 122 | "script": `ctx._source.text = "some text"`, 123 | }, 124 | }) 125 | 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | if response.Error != "" { 131 | t.Error(response.Error) 132 | } 133 | 134 | if expected, got := 2, response.Version; expected != got { 135 | t.Errorf("expected version to be %d; got %d", expected, got) 136 | } 137 | } 138 | 139 | func TestClusterDelete(t *testing.T) { 140 | c := newCluster(t, []string{"twitter"}, map[string]interface{}{ 141 | "/twitter/tweet/1": map[string]string{ 142 | "name": "John", 143 | }, 144 | }) 145 | defer c.Shutdown() 146 | defer deleteIndices(t, []string{"twitter"}) 147 | 148 | response, err := c.Delete(es.DeleteRequest{ 149 | es.IndexParams{ 150 | Index: "twitter", 151 | Type: "tweet", 152 | Id: "1", 153 | Refresh: "true", 154 | }, 155 | }) 156 | 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | if response.Error != "" { 162 | t.Error(response.Error) 163 | } 164 | 165 | if expected, got := 2, response.Version; expected != got { 166 | t.Errorf("expected version to be %d; got %d", expected, got) 167 | } 168 | } 169 | 170 | func TestClusterBulk(t *testing.T) { 171 | c := newCluster(t, []string{"twitter"}, map[string]interface{}{ 172 | "/twitter/tweet/1": map[string]string{ 173 | "name": "John", 174 | }, 175 | }) 176 | defer c.Shutdown() 177 | defer deleteIndices(t, []string{"twitter"}) 178 | 179 | response, err := c.Bulk(es.BulkRequest{ 180 | es.BulkParams{Refresh: "true"}, 181 | []es.BulkIndexable{ 182 | es.IndexRequest{ 183 | es.IndexParams{Index: "twitter", Type: "tweet", Id: "1"}, 184 | map[string]interface{}{"name": "James"}, 185 | }, 186 | es.DeleteRequest{ 187 | es.IndexParams{Index: "twitter", Type: "tweet", Id: "2"}, 188 | }, 189 | es.CreateRequest{ 190 | es.IndexParams{Index: "twitter", Type: "tweet", Id: "3"}, 191 | map[string]interface{}{"name": "John"}, 192 | }, 193 | }, 194 | }) 195 | 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | 200 | if len(response.Items) != 3 { 201 | t.Fatalf("expected 3 responses, got %d", len(response.Items)) 202 | } 203 | 204 | if expected, got := 2, response.Items[0].Version; expected != got { 205 | t.Errorf("expected version of doc to be %d; got %d", expected, got) 206 | } 207 | 208 | if expected, got := false, response.Items[1].Found; expected != got { 209 | t.Errorf("expected delete op to return found = false") 210 | } 211 | 212 | if expected, got := 1, response.Items[2].Version; expected != got { 213 | t.Errorf("expected version of doc to be %d; got %d", expected, got) 214 | } 215 | } 216 | 217 | func TestSimpleTermQuery(t *testing.T) { 218 | indices := []string{"twitter"} 219 | c := newCluster(t, indices, map[string]interface{}{ 220 | "/twitter/tweet/1": map[string]string{ 221 | "user": "kimchy", 222 | "post_date": "2009-11-15T14:12:12", 223 | "message": "trying out Elastic Search", 224 | }, 225 | }) 226 | defer c.Shutdown() 227 | defer deleteIndices(t, indices) // comment out to leave data after test 228 | 229 | q := es.QueryWrapper(es.TermQuery(es.TermQueryParams{ 230 | Query: &es.Wrapper{ 231 | Name: "user", 232 | Wrapped: "kimchy", 233 | }, 234 | })) 235 | 236 | request := es.SearchRequest{ 237 | es.SearchParams{ 238 | Indices: []string{"twitter"}, 239 | Types: []string{"tweet"}, 240 | }, 241 | q, 242 | } 243 | 244 | response, err := c.Search(request) 245 | if err != nil { 246 | t.Error(err) 247 | } 248 | 249 | if response.Error != "" { 250 | t.Error(response.Error) 251 | } 252 | if expected, got := 1, response.HitsWrapper.Total; expected != got { 253 | t.Fatalf("expected %d, got %d", expected, got) 254 | } 255 | 256 | t.Logf("OK, %d hit(s), %dms", response.HitsWrapper.Total, response.Took) 257 | } 258 | 259 | func TestMultiSearch(t *testing.T) { 260 | indices := []string{"index1", "index2"} 261 | c := newCluster(t, indices, map[string]interface{}{ 262 | "/index1/foo/1": map[string]string{ 263 | "user": "alice", 264 | "description": "index=index1 type=foo id=1 user=alice", 265 | }, 266 | "/index2/bar/2": map[string]string{ 267 | "user": "bob", 268 | "description": "index=index2 type=bar id=2 user=bob", 269 | }, 270 | }) 271 | defer c.Shutdown() 272 | defer deleteIndices(t, indices) // comment out to leave data after test 273 | 274 | q1 := es.QueryWrapper(es.TermQuery(es.TermQueryParams{ 275 | Query: &es.Wrapper{ 276 | Name: "user", 277 | Wrapped: "alice", 278 | }, 279 | })) 280 | q2 := es.QueryWrapper(es.TermQuery(es.TermQueryParams{ 281 | Query: &es.Wrapper{ 282 | Name: "user", 283 | Wrapped: "bob", 284 | }, 285 | })) 286 | q3 := es.QueryWrapper(es.MatchAllQuery()) 287 | 288 | request := es.MultiSearchRequest{ 289 | Requests: []es.SearchRequest{ 290 | es.SearchRequest{ 291 | es.SearchParams{ 292 | Indices: []string{"index1"}, 293 | Types: []string{"foo"}, 294 | }, 295 | q1, 296 | }, 297 | es.SearchRequest{ 298 | es.SearchParams{ 299 | Indices: []string{"index2"}, 300 | Types: []string{"bar"}, 301 | }, 302 | q2, 303 | }, 304 | es.SearchRequest{ 305 | es.SearchParams{ 306 | Indices: []string{}, // "index1", "index2" is not supported (!) 307 | Types: []string{}, // "type1", "type2" is not supported (!) 308 | }, 309 | q3, 310 | }, 311 | }, 312 | } 313 | 314 | response, err := c.MultiSearch(request) 315 | if err != nil { 316 | t.Fatal(err) 317 | } 318 | 319 | if expected, got := 3, len(response.Responses); expected != got { 320 | t.Fatalf("expected %d response(s), got %d", expected, got) 321 | } 322 | 323 | r1 := response.Responses[0] 324 | if r1.Error != "" { 325 | t.Fatalf("response 1: %s", r1.Error) 326 | } 327 | if expected, got := 1, r1.HitsWrapper.Total; expected != got { 328 | t.Fatalf("response 1: expected %d hit(s), got %d", expected, got) 329 | } 330 | buf, _ := json.Marshal(r1) 331 | t.Logf("response 1 OK: %s", buf) 332 | 333 | r2 := response.Responses[1] 334 | if r2.Error != "" { 335 | t.Fatalf("response 2: %s", r1.Error) 336 | } 337 | if expected, got := 1, r2.HitsWrapper.Total; expected != got { 338 | t.Fatalf("response 2: expected %d hit(s), got %d", expected, got) 339 | } 340 | buf, _ = json.Marshal(r2) 341 | t.Logf("response 2 OK: %s", buf) 342 | 343 | r3 := response.Responses[2] 344 | if r3.Error != "" { 345 | t.Fatalf("response 3: %s", r1.Error) 346 | } 347 | if expected, got := 2, r3.HitsWrapper.Total; expected != got { 348 | t.Fatalf("response 3: expected %d hit(s), got %d", expected, got) 349 | } 350 | buf, _ = json.Marshal(r3) 351 | t.Logf("response 3 OK: %s", buf) 352 | } 353 | 354 | func TestConstantScoreNoScore(t *testing.T) { 355 | indices := []string{"twitter"} 356 | c := newCluster(t, indices, map[string]interface{}{ 357 | "/twitter/tweet/1": map[string]string{ 358 | "user": "kimchy", 359 | "post_date": "2009-11-15T14:12:12", 360 | "message": "trying out Elastic Search", 361 | }, 362 | }) 363 | defer c.Shutdown() 364 | defer deleteIndices(t, indices) // comment out to leave data after test 365 | 366 | q := map[string]interface{}{ 367 | "size": 20, 368 | "sort": []string{"post_date"}, 369 | "filter": map[string]interface{}{ 370 | "and": []map[string]interface{}{ 371 | map[string]interface{}{ 372 | "type": map[string]string{"value": "tweet"}, 373 | }, 374 | }, 375 | }, 376 | "query": map[string]interface{}{ 377 | "constant_score": map[string]interface{}{ 378 | "filter": map[string]interface{}{ 379 | "term": map[string]string{"user": "kimchy"}, 380 | }, 381 | }, 382 | }, 383 | } 384 | 385 | request := es.SearchRequest{ 386 | es.SearchParams{ 387 | Indices: []string{"twitter"}, 388 | Types: []string{"tweet"}, 389 | }, 390 | q, 391 | } 392 | 393 | response, err := c.Search(request) 394 | if err != nil { 395 | t.Fatalf("Search: %s", err) 396 | } 397 | 398 | buf, _ := json.Marshal(response) 399 | t.Logf("got response: %s", buf) 400 | 401 | if response.Error != "" { 402 | t.Error(response.Error) 403 | } 404 | if expected, got := 1, response.HitsWrapper.Total; expected != got { 405 | t.Fatalf("expected %d, got %d", expected, got) 406 | } 407 | 408 | if response.HitsWrapper.Hits[0].Score != nil { 409 | t.Fatalf("score: expected nil, got something") 410 | } 411 | 412 | t.Logf("OK, %d hit(s), %dms", response.HitsWrapper.Total, response.Took) 413 | } 414 | 415 | // 416 | // 417 | // 418 | 419 | func waitForCluster(timeout time.Duration) { 420 | giveUp := time.After(timeout) 421 | delay := 100 * time.Millisecond 422 | for { 423 | _, err := http.Get("http://127.0.0.1:9200") 424 | if err == nil { 425 | fmt.Printf("ElasticSearch now available\n") 426 | return // great 427 | } 428 | 429 | fmt.Printf("ElasticSearch not ready yet; waiting %s\n", delay) 430 | select { 431 | case <-time.After(delay): 432 | delay *= 2 433 | case <-giveUp: 434 | panic("ElasticSearch didn't come up in time") 435 | } 436 | } 437 | } 438 | 439 | func newCluster(t *testing.T, indices []string, m map[string]interface{}) *es.Cluster { 440 | deleteIndices(t, indices) 441 | loadData(t, m) 442 | 443 | endpoints := []string{"http://localhost:9200"} 444 | pingInterval, pingTimeout := 10*time.Second, 3*time.Second 445 | return es.NewCluster(endpoints, pingInterval, pingTimeout) 446 | } 447 | 448 | func deleteIndices(t *testing.T, indices []string) { 449 | for _, index := range indices { 450 | // refresh=true to make document(s) immediately deleted 451 | url := "http://127.0.0.1:9200/" + index + "?refresh=true" 452 | req, err := http.NewRequest("DELETE", url, nil) 453 | if err != nil { 454 | t.Fatal(err) 455 | } 456 | 457 | resp, err := http.DefaultClient.Do(req) 458 | if err != nil { 459 | t.Fatal(err) 460 | } 461 | 462 | respBuf, err := ioutil.ReadAll(resp.Body) 463 | resp.Body.Close() 464 | if err != nil { 465 | t.Fatal(err) 466 | } 467 | 468 | t.Logf("DELETE %s: %s", index, respBuf) 469 | } 470 | } 471 | 472 | func loadData(t *testing.T, m map[string]interface{}) { 473 | for path, body := range m { 474 | reqBytes, err := json.Marshal(body) 475 | if err != nil { 476 | t.Fatal(err) 477 | } 478 | 479 | // refresh=true to make document(s) immediately searchable 480 | url := "http://127.0.0.1:9200" + path + "?refresh=true" 481 | req, err := http.NewRequest("PUT", url, bytes.NewBuffer(reqBytes)) 482 | if err != nil { 483 | t.Fatal(err) 484 | } 485 | 486 | resp, err := http.DefaultClient.Do(req) 487 | if err != nil { 488 | t.Fatal(err) 489 | } 490 | 491 | respBuf, err := ioutil.ReadAll(resp.Body) 492 | resp.Body.Close() 493 | if err != nil { 494 | t.Fatal(err) 495 | } 496 | 497 | t.Logf("PUT %s: %s", path, respBuf) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /index.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "path" 10 | ) 11 | 12 | type BulkResponse struct { 13 | Took int `json:"took"` // ms 14 | 15 | Items []BulkItemResponse `json:"items"` 16 | } 17 | 18 | type BulkItemResponse IndexResponse 19 | 20 | // Bulk responses are wrapped in an extra object whose only key is the 21 | // operation performed (create, delete, or index). BulkItemResponse response is 22 | // an alias for IndexResponse, but deals with this extra indirection. 23 | func (r *BulkItemResponse) UnmarshalJSON(data []byte) error { 24 | var wrapper struct { 25 | Create json.RawMessage `json:"create"` 26 | Delete json.RawMessage `json:"delete"` 27 | Index json.RawMessage `json:"index"` 28 | } 29 | 30 | if err := json.Unmarshal(data, &wrapper); err != nil { 31 | return err 32 | } 33 | 34 | var inner json.RawMessage 35 | 36 | switch { 37 | case wrapper.Create != nil: 38 | inner = wrapper.Create 39 | case wrapper.Index != nil: 40 | inner = wrapper.Index 41 | case wrapper.Delete != nil: 42 | inner = wrapper.Delete 43 | default: 44 | return fmt.Errorf("expected bulk response to be create, index, or delete") 45 | } 46 | 47 | if err := json.Unmarshal(inner, (*IndexResponse)(r)); err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | type IndexResponse struct { 55 | Found bool `json:"found"` 56 | ID string `json:"_id"` 57 | Index string `json:"_index"` 58 | OK bool `json:"ok"` 59 | Type string `json:"_type"` 60 | Version int `json:"_version"` 61 | 62 | Error string `json:"error,omitempty"` 63 | Status int `json:"status,omitempty"` 64 | TimedOut bool `json:"timed_out,omitempty"` 65 | } 66 | 67 | type IndexParams struct { 68 | Index string `json:"_index"` 69 | Type string `json:"_type"` 70 | Id string `json:"_id"` 71 | 72 | Consistency string `json:"_consistency,omitempty"` 73 | Parent string `json:"_parent,omitempty"` 74 | Percolate string `json:"_percolate,omitempty"` 75 | Refresh string `json:"_refresh,omitempty"` 76 | Replication string `json:"_replication,omitempty"` 77 | Routing string `json:"_routing,omitempty"` 78 | TTL string `json:"_ttl,omitempty"` 79 | Timestamp string `json:"_timestamp,omitempty"` 80 | Version string `json:"_version,omitempty"` 81 | VersionType string `json:"_version_type,omitempty"` 82 | } 83 | 84 | func (p IndexParams) Values() url.Values { 85 | return values(map[string]string{ 86 | "consistency": p.Consistency, 87 | "parent": p.Parent, 88 | "percolate": p.Percolate, 89 | "refresh": p.Refresh, 90 | "replication": p.Replication, 91 | "routing": p.Routing, 92 | "ttl": p.TTL, 93 | "timestamp": p.Timestamp, 94 | "version": p.Version, 95 | "version_type": p.VersionType, 96 | }) 97 | } 98 | 99 | type IndexRequest struct { 100 | Params IndexParams 101 | Source interface{} 102 | } 103 | 104 | func (r IndexRequest) EncodeBulkHeader(enc *json.Encoder) error { 105 | return enc.Encode(map[string]IndexParams{ 106 | "index": r.Params, 107 | }) 108 | } 109 | 110 | func (r IndexRequest) EncodeSource(enc *json.Encoder) error { 111 | return enc.Encode(r.Source) 112 | } 113 | 114 | func (r IndexRequest) Request(uri *url.URL) (*http.Request, error) { 115 | uri.Path = path.Join("/", r.Params.Index, r.Params.Type, r.Params.Id) 116 | uri.RawQuery = r.Params.Values().Encode() 117 | 118 | buf := new(bytes.Buffer) 119 | enc := json.NewEncoder(buf) 120 | 121 | if err := r.EncodeSource(enc); err != nil { 122 | return nil, err 123 | } 124 | 125 | return http.NewRequest("PUT", uri.String(), buf) 126 | } 127 | 128 | type CreateRequest struct { 129 | Params IndexParams 130 | Source interface{} 131 | } 132 | 133 | func (r CreateRequest) EncodeBulkHeader(enc *json.Encoder) error { 134 | return enc.Encode(map[string]IndexParams{ 135 | "create": r.Params, 136 | }) 137 | } 138 | 139 | func (r CreateRequest) EncodeSource(enc *json.Encoder) error { 140 | return enc.Encode(r.Source) 141 | } 142 | 143 | func (r CreateRequest) Request(uri *url.URL) (*http.Request, error) { 144 | uri.Path = path.Join("/", r.Params.Index, r.Params.Type, r.Params.Id, "_create") 145 | uri.RawQuery = r.Params.Values().Encode() 146 | 147 | buf := new(bytes.Buffer) 148 | enc := json.NewEncoder(buf) 149 | 150 | if err := r.EncodeSource(enc); err != nil { 151 | return nil, err 152 | } 153 | 154 | return http.NewRequest("PUT", uri.String(), buf) 155 | } 156 | 157 | type DeleteRequest struct { 158 | Params IndexParams 159 | } 160 | 161 | func (r DeleteRequest) EncodeBulkHeader(enc *json.Encoder) error { 162 | return enc.Encode(map[string]IndexParams{ 163 | "delete": r.Params, 164 | }) 165 | } 166 | 167 | func (r DeleteRequest) EncodeSource(enc *json.Encoder) error { 168 | return nil 169 | } 170 | 171 | func (r DeleteRequest) Request(uri *url.URL) (*http.Request, error) { 172 | uri.Path = path.Join("/", r.Params.Index, r.Params.Type, r.Params.Id) 173 | uri.RawQuery = r.Params.Values().Encode() 174 | 175 | return http.NewRequest("DELETE", uri.String(), nil) 176 | } 177 | 178 | type UpdateRequest struct { 179 | Params IndexParams 180 | Source interface{} 181 | } 182 | 183 | func (r UpdateRequest) Request(uri *url.URL) (*http.Request, error) { 184 | uri.Path = path.Join("/", r.Params.Index, r.Params.Type, r.Params.Id, "_update") 185 | uri.RawQuery = r.Params.Values().Encode() 186 | 187 | buf := new(bytes.Buffer) 188 | 189 | if err := json.NewEncoder(buf).Encode(r.Source); err != nil { 190 | return nil, err 191 | } 192 | 193 | return http.NewRequest("POST", uri.String(), buf) 194 | } 195 | 196 | // 197 | // 198 | // 199 | 200 | type BulkParams struct { 201 | Consistency string 202 | Refresh string 203 | Replication string 204 | } 205 | 206 | func (p BulkParams) Values() url.Values { 207 | return values(map[string]string{ 208 | "consistency": p.Consistency, 209 | "refresh": p.Refresh, 210 | "replication": p.Replication, 211 | }) 212 | } 213 | 214 | type BulkIndexable interface { 215 | EncodeBulkHeader(*json.Encoder) error 216 | EncodeSource(*json.Encoder) error 217 | } 218 | 219 | type BulkRequest struct { 220 | Params BulkParams 221 | Requests []BulkIndexable 222 | } 223 | 224 | func (r BulkRequest) Request(uri *url.URL) (*http.Request, error) { 225 | uri.Path = "/_bulk" 226 | uri.RawQuery = r.Params.Values().Encode() 227 | 228 | buf := new(bytes.Buffer) 229 | enc := json.NewEncoder(buf) 230 | 231 | for _, req := range r.Requests { 232 | if err := req.EncodeBulkHeader(enc); err != nil { 233 | return nil, err 234 | } 235 | 236 | if err := req.EncodeSource(enc); err != nil { 237 | return nil, err 238 | } 239 | } 240 | 241 | return http.NewRequest("PUT", uri.String(), buf) 242 | } 243 | -------------------------------------------------------------------------------- /index_test.go: -------------------------------------------------------------------------------- 1 | package elasticsearch_test 2 | 3 | import ( 4 | "encoding/json" 5 | es "github.com/peterbourgon/elasticsearch" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func TestIndexRequest(t *testing.T) { 11 | doc := map[string]string{ 12 | "user": "kimchy", 13 | "post_date": "2009-11-15T14:12:12", 14 | "message": "trying out Elastic Search", 15 | } 16 | 17 | request, err := es.IndexRequest{ 18 | es.IndexParams{ 19 | Index: "twitter", 20 | Type: "tweet", 21 | Id: "1", 22 | Percolate: "*", 23 | Version: "4", 24 | }, 25 | doc, 26 | }.Request(&url.URL{}) 27 | 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | if expected, got := "PUT", request.Method; expected != got { 33 | t.Errorf("expected method = %q; got %q", expected, got) 34 | } 35 | 36 | if expected, got := "/twitter/tweet/1", request.URL.Path; expected != got { 37 | t.Errorf("expected path = %q; got %q", expected, got) 38 | } 39 | 40 | q := request.URL.Query() 41 | 42 | if expected, got := "*", q.Get("percolate"); expected != got { 43 | t.Errorf("expected percolate = %q; got %q", expected, got) 44 | } 45 | 46 | if expected, got := "4", q.Get("version"); expected != got { 47 | t.Errorf("expected version = %q; got %q", expected, got) 48 | } 49 | 50 | var body struct { 51 | User string `json:"user"` 52 | PostDate string `json:"post_date"` 53 | Message string `json:"message"` 54 | } 55 | 56 | if err := json.NewDecoder(request.Body).Decode(&body); err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if expected, got := doc["user"], body.User; expected != got { 61 | t.Errorf("expected user = %q; got %q", expected, got) 62 | } 63 | 64 | if expected, got := doc["post_date"], body.PostDate; expected != got { 65 | t.Errorf("expected post_date = %q; got %q", expected, got) 66 | } 67 | 68 | if expected, got := doc["message"], body.Message; expected != got { 69 | t.Errorf("expected message = %q; got %q", expected, got) 70 | } 71 | } 72 | 73 | func TestCreateRequest(t *testing.T) { 74 | doc := map[string]string{ 75 | "user": "kimchy", 76 | "post_date": "2009-11-15T14:12:12", 77 | "message": "trying out Elastic Search", 78 | } 79 | 80 | request, err := es.CreateRequest{ 81 | es.IndexParams{ 82 | Index: "twitter", 83 | Type: "tweet", 84 | Id: "1", 85 | Percolate: "*", 86 | Version: "4", 87 | }, 88 | doc, 89 | }.Request(&url.URL{}) 90 | 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | if expected, got := "PUT", request.Method; expected != got { 96 | t.Errorf("expected method = %q; got %q", expected, got) 97 | } 98 | 99 | if expected, got := "/twitter/tweet/1/_create", request.URL.Path; expected != got { 100 | t.Errorf("expected path = %q; got %q", expected, got) 101 | } 102 | 103 | q := request.URL.Query() 104 | 105 | if expected, got := "*", q.Get("percolate"); expected != got { 106 | t.Errorf("expected percolate = %q; got %q", expected, got) 107 | } 108 | 109 | if expected, got := "4", q.Get("version"); expected != got { 110 | t.Errorf("expected version = %q; got %q", expected, got) 111 | } 112 | 113 | var body struct { 114 | User string `json:"user"` 115 | PostDate string `json:"post_date"` 116 | Message string `json:"message"` 117 | } 118 | 119 | if err := json.NewDecoder(request.Body).Decode(&body); err != nil { 120 | t.Fatal(err) 121 | } 122 | 123 | if expected, got := doc["user"], body.User; expected != got { 124 | t.Errorf("expected user = %q; got %q", expected, got) 125 | } 126 | 127 | if expected, got := doc["post_date"], body.PostDate; expected != got { 128 | t.Errorf("expected post_date = %q; got %q", expected, got) 129 | } 130 | 131 | if expected, got := doc["message"], body.Message; expected != got { 132 | t.Errorf("expected message = %q; got %q", expected, got) 133 | } 134 | } 135 | 136 | func TestUpdateRequest(t *testing.T) { 137 | doc := map[string]string{ 138 | "script": `ctx._source.text = "some text"`, 139 | } 140 | 141 | request, err := es.UpdateRequest{ 142 | es.IndexParams{ 143 | Index: "twitter", 144 | Type: "tweet", 145 | Id: "1", 146 | Percolate: "*", 147 | Version: "4", 148 | }, 149 | doc, 150 | }.Request(&url.URL{}) 151 | 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | 156 | if expected, got := "POST", request.Method; expected != got { 157 | t.Errorf("expected method = %q; got %q", expected, got) 158 | } 159 | 160 | if expected, got := "/twitter/tweet/1/_update", request.URL.Path; expected != got { 161 | t.Errorf("expected path = %q; got %q", expected, got) 162 | } 163 | 164 | q := request.URL.Query() 165 | 166 | if expected, got := "*", q.Get("percolate"); expected != got { 167 | t.Errorf("expected percolate = %q; got %q", expected, got) 168 | } 169 | 170 | if expected, got := "4", q.Get("version"); expected != got { 171 | t.Errorf("expected version = %q; got %q", expected, got) 172 | } 173 | 174 | var body struct { 175 | Script string `json:"script"` 176 | } 177 | 178 | if err := json.NewDecoder(request.Body).Decode(&body); err != nil { 179 | t.Fatal(err) 180 | } 181 | 182 | if expected, got := doc["script"], body.Script; expected != got { 183 | t.Errorf("expected user = %q; got %q", expected, got) 184 | } 185 | } 186 | 187 | func TestDeleteRequest(t *testing.T) { 188 | request, err := es.DeleteRequest{ 189 | es.IndexParams{ 190 | Index: "twitter", 191 | Type: "tweet", 192 | Id: "1", 193 | Percolate: "*", 194 | Version: "4", 195 | }, 196 | }.Request(&url.URL{}) 197 | 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | 202 | if expected, got := "DELETE", request.Method; expected != got { 203 | t.Errorf("expected method = %q; got %q", expected, got) 204 | } 205 | 206 | if expected, got := "/twitter/tweet/1", request.URL.Path; expected != got { 207 | t.Errorf("expected path = %q; got %q", expected, got) 208 | } 209 | 210 | q := request.URL.Query() 211 | 212 | if expected, got := "*", q.Get("percolate"); expected != got { 213 | t.Errorf("expected percolate = %q; got %q", expected, got) 214 | } 215 | 216 | if expected, got := "4", q.Get("version"); expected != got { 217 | t.Errorf("expected version = %q; got %q", expected, got) 218 | } 219 | 220 | if request.Body != nil { 221 | t.Errorf("expected request to have an empty body") 222 | } 223 | } 224 | 225 | func TestBulkRequest(t *testing.T) { 226 | request, err := es.BulkRequest{ 227 | es.BulkParams{ 228 | Consistency: "quorum", 229 | }, 230 | []es.BulkIndexable{ 231 | es.IndexRequest{ 232 | es.IndexParams{ 233 | Index: "twitter", 234 | Type: "tweet", 235 | Id: "1", 236 | Routing: "foo", 237 | }, 238 | map[string]string{"user": "kimchy"}, 239 | }, 240 | es.CreateRequest{ 241 | es.IndexParams{ 242 | Index: "twitter", 243 | Type: "tweet", 244 | Id: "2", 245 | Version: "2", 246 | }, 247 | map[string]string{"user": "kimchy2"}, 248 | }, 249 | es.DeleteRequest{ 250 | es.IndexParams{ 251 | Index: "twitter", 252 | Type: "tweet", 253 | Id: "1", 254 | }, 255 | }, 256 | }, 257 | }.Request(&url.URL{}) 258 | 259 | if err != nil { 260 | t.Fatal(err) 261 | } 262 | 263 | if expected, got := "PUT", request.Method; expected != got { 264 | t.Errorf("expected method = %q; got %q", expected, got) 265 | } 266 | 267 | if expected, got := "/_bulk", request.URL.Path; expected != got { 268 | t.Errorf("expected path = %q; got %q", expected, got) 269 | } 270 | 271 | q := request.URL.Query() 272 | 273 | if expected, got := "quorum", q.Get("consistency"); expected != got { 274 | t.Errorf("expected percolate = %q; got %q", expected, got) 275 | } 276 | 277 | type actionMetadata struct { 278 | Index map[string]string `json:"index"` 279 | Create map[string]string `json:"create"` 280 | Delete map[string]string `json:"delete"` 281 | } 282 | 283 | header := actionMetadata{} 284 | body := map[string]string{} 285 | decoder := json.NewDecoder(request.Body) 286 | 287 | if err := decoder.Decode(&header); err != nil { 288 | t.Fatal(err) 289 | } 290 | 291 | if header.Index == nil { 292 | t.Fatal("index metadata was not encoded") 293 | } 294 | 295 | if expected, got := "twitter", header.Index["_index"]; expected != got { 296 | t.Errorf("expected _index = %q; got %q", expected, got) 297 | } 298 | 299 | if expected, got := "tweet", header.Index["_type"]; expected != got { 300 | t.Errorf("expected _type = %q; got %q", expected, got) 301 | } 302 | 303 | if expected, got := "1", header.Index["_id"]; expected != got { 304 | t.Errorf("expected _id = %q; got %q", expected, got) 305 | } 306 | 307 | if expected, got := "foo", header.Index["_routing"]; expected != got { 308 | t.Errorf("expected _id = %q; got %q", expected, got) 309 | } 310 | 311 | if err := decoder.Decode(&body); err != nil { 312 | t.Fatal(err) 313 | } 314 | 315 | if expected, got := "kimchy", body["user"]; expected != got { 316 | t.Errorf("expected user = %q; got %q", expected, got) 317 | } 318 | 319 | if err := decoder.Decode(&header); err != nil { 320 | t.Fatal(err) 321 | } 322 | 323 | if header.Create == nil { 324 | t.Fatal("create metadata was not encoded") 325 | } 326 | 327 | if expected, got := "twitter", header.Create["_index"]; expected != got { 328 | t.Errorf("expected _index = %q; got %q", expected, got) 329 | } 330 | 331 | if expected, got := "tweet", header.Create["_type"]; expected != got { 332 | t.Errorf("expected _type = %q; got %q", expected, got) 333 | } 334 | 335 | if expected, got := "2", header.Create["_id"]; expected != got { 336 | t.Errorf("expected _id = %q; got %q", expected, got) 337 | } 338 | 339 | if expected, got := "2", header.Create["_version"]; expected != got { 340 | t.Errorf("expected _id = %q; got %q", expected, got) 341 | } 342 | 343 | if err := decoder.Decode(&body); err != nil { 344 | t.Fatal(err) 345 | } 346 | 347 | if expected, got := "kimchy2", body["user"]; expected != got { 348 | t.Errorf("expected user = %q; got %q", expected, got) 349 | } 350 | 351 | if err := decoder.Decode(&header); err != nil { 352 | t.Fatal(err) 353 | } 354 | 355 | if header.Delete == nil { 356 | t.Fatal("delete metadata was not encoded") 357 | } 358 | 359 | if expected, got := "twitter", header.Delete["_index"]; expected != got { 360 | t.Errorf("expected _index = %q; got %q", expected, got) 361 | } 362 | 363 | if expected, got := "tweet", header.Delete["_type"]; expected != got { 364 | t.Errorf("expected _type = %q; got %q", expected, got) 365 | } 366 | 367 | if expected, got := "1", header.Delete["_id"]; expected != got { 368 | t.Errorf("expected _id = %q; got %q", expected, got) 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // A Node is a structure which represents a single ElasticSearch host. 16 | type Node struct { 17 | sync.RWMutex 18 | endpoint string 19 | health Health 20 | client *http.Client // default http client 21 | pingClient *http.Client // used for Ping() only 22 | } 23 | 24 | // NewNode constructs a Node handle. The endpoint should be of the form 25 | // "scheme://host:port", eg. "http://es001:9200". 26 | // 27 | // The ping interval is dictated at a higher level (the Cluster), but individual 28 | // ping timeouts are stored with the Nodes themselves, in a custom HTTP client, 29 | // with a timeout as part of the Transport dialer. This custom pingClient is 30 | // used exclusively for Ping() calls. 31 | // 32 | // Regular queries are made with the default client http.Client, which has 33 | // no explicit timeout set in the Transport dialer. 34 | func NewNode(endpoint string, pingTimeout time.Duration) *Node { 35 | return &Node{ 36 | endpoint: endpoint, 37 | health: Yellow, 38 | client: &http.Client{ 39 | Transport: &http.Transport{ 40 | MaxIdleConnsPerHost: 250, 41 | }, 42 | }, 43 | pingClient: &http.Client{ 44 | Transport: &http.Transport{ 45 | Dial: timeoutDialer(pingTimeout), 46 | }, 47 | }, 48 | } 49 | } 50 | 51 | // Ping attempts to HTTP GET a specific endpoint, parse some kind of 52 | // status indicator, and returns true if everything was successful. 53 | func (n *Node) Ping() bool { 54 | u, err := url.Parse(n.endpoint) 55 | if err != nil { 56 | log.Printf("ElasticSearch: ping: resolve: %s", err) 57 | return false 58 | } 59 | u.Path = "/_cluster/nodes/_local" // some arbitrary, reasonable endpoint 60 | 61 | resp, err := n.pingClient.Get(u.String()) 62 | if err != nil { 63 | log.Printf("ElasticSearch: ping %s: GET: %s", u.Host, err) 64 | return false 65 | } 66 | defer resp.Body.Close() 67 | 68 | var status struct { 69 | OK bool `json:"ok"` 70 | } 71 | 72 | if err = json.NewDecoder(resp.Body).Decode(&status); err != nil { 73 | log.Printf("ElasticSearch: ping %s: %s", u.Host, err) 74 | return false 75 | } 76 | 77 | if !status.OK { 78 | log.Printf("ElasticSearch: ping %s: ok=false", u.Host) 79 | return false 80 | } 81 | 82 | return true 83 | } 84 | 85 | // PingAndSet performs a Ping, and updates the Node's health accordingly. 86 | func (n *Node) pingAndSet() { 87 | success := n.Ping() 88 | func() { 89 | n.Lock() 90 | defer n.Unlock() 91 | if success { 92 | n.health = n.health.Improve() 93 | } else { 94 | n.health = n.health.Degrade() 95 | } 96 | }() 97 | } 98 | 99 | // GetHealth returns the health of the node, for use in the Cluster's GetBest. 100 | func (n *Node) GetHealth() Health { 101 | n.RLock() 102 | defer n.RUnlock() 103 | return n.health 104 | } 105 | 106 | // Executes the Fireable f against the node and decodes the server's reply into 107 | // response. 108 | func (n *Node) Execute(f Fireable, response interface{}) error { 109 | uri, err := url.Parse(n.endpoint) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | request, err := f.Request(uri) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | r, err := n.client.Do(request) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | defer r.Body.Close() 125 | 126 | return json.NewDecoder(r.Body).Decode(response) 127 | } 128 | 129 | // 130 | // 131 | // 132 | 133 | type Nodes []*Node 134 | 135 | // PingAll triggers simultaneous PingAndSets across all Nodes, 136 | // and blocks until they've all completed. 137 | func (n Nodes) pingAll() { 138 | c := make(chan bool, len(n)) 139 | for _, node := range n { 140 | go func(tgt *Node) { tgt.pingAndSet(); c <- true }(node) 141 | } 142 | for i := 0; i < cap(c); i++ { 143 | <-c 144 | } 145 | } 146 | 147 | // GetBest returns the "best" Node, as decided by each Node's health. 148 | // It's possible that no Node will be healthy enough to be returned. 149 | // In that case, GetBest returns an error, and processing cannot continue. 150 | func (n Nodes) getBest() (*Node, error) { 151 | green, yellow := []*Node{}, []*Node{} 152 | for _, node := range n { 153 | switch node.GetHealth() { 154 | case Green: 155 | green = append(green, node) 156 | case Yellow: 157 | yellow = append(yellow, node) 158 | } 159 | } 160 | 161 | if len(green) > 0 { 162 | return green[rand.Intn(len(green))], nil 163 | } 164 | 165 | if len(yellow) > 0 { 166 | return yellow[rand.Intn(len(yellow))], nil 167 | } 168 | 169 | return nil, fmt.Errorf("no healthy nodes available") 170 | } 171 | 172 | // 173 | // 174 | // 175 | 176 | // Health is some encoding of the perceived state of a Node. 177 | // A Cluster should favor sending queries against healthier nodes. 178 | type Health int 179 | 180 | const ( 181 | Green Health = iota // resemblance to cluster health codes is coincidental 182 | Yellow 183 | Red 184 | ) 185 | 186 | func (h Health) String() string { 187 | switch h { 188 | case Green: 189 | return "Green" 190 | case Yellow: 191 | return "Yellow" 192 | case Red: 193 | return "Red" 194 | } 195 | panic("unreachable") 196 | } 197 | 198 | func (h Health) Improve() Health { 199 | switch h { 200 | case Red: 201 | return Yellow 202 | default: 203 | return Green 204 | } 205 | panic("unreachable") 206 | } 207 | 208 | func (h Health) Degrade() Health { 209 | switch h { 210 | case Green: 211 | return Yellow 212 | default: 213 | return Red 214 | } 215 | panic("unreachable") 216 | } 217 | 218 | // 219 | // 220 | // 221 | 222 | // timeoutDialer returns a function that can be put into an HTTP Client's 223 | // Transport, which will cause all requests made on that client to abort 224 | // if they're not handled within the passed duration. 225 | func timeoutDialer(d time.Duration) func(net, addr string) (net.Conn, error) { 226 | return func(netw, addr string) (net.Conn, error) { 227 | c, err := net.Dial(netw, addr) 228 | if err != nil { 229 | return nil, err 230 | } 231 | c.SetDeadline(time.Now().Add(d)) 232 | return c, nil 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package elasticsearch_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | es "github.com/peterbourgon/elasticsearch" 7 | ) 8 | 9 | func marshalOrError(q es.SubQuery) string { 10 | buf, err := json.Marshal(q) 11 | if err != nil { 12 | return err.Error() 13 | } 14 | return string(buf) 15 | } 16 | 17 | // http://www.elasticsearch.org/guide/reference/query-dsl/term-query.html 18 | func ExampleBasicTermQuery() { 19 | q := es.TermQuery(es.TermQueryParams{ 20 | Query: &es.Wrapper{ 21 | Name: "user", 22 | Wrapped: "kimchy", 23 | }, 24 | }) 25 | 26 | fmt.Print(marshalOrError(q)) 27 | // Output: 28 | // {"term":{"user":"kimchy"}} 29 | } 30 | -------------------------------------------------------------------------------- /requests.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | // Helper function which turns a map of strings into url.Values, omitting empty 13 | // values. 14 | func values(v map[string]string) url.Values { 15 | values := url.Values{} 16 | 17 | for key, value := range v { 18 | if value != "" { 19 | values.Set(key, value) 20 | } 21 | } 22 | 23 | return values 24 | } 25 | 26 | // Fireable defines anything which can be fired against the search cluster. 27 | type Fireable interface { 28 | Request(uri *url.URL) (*http.Request, error) 29 | } 30 | 31 | // 32 | // 33 | // 34 | 35 | type SearchParams struct { 36 | Indices []string `json:"index,omitempty"` 37 | Types []string `json:"type,omitempty"` 38 | 39 | Routing string `json:"routing,omitempty"` 40 | Preference string `json:"preference,omitempty"` 41 | SearchType string `json:"search_type,omitempty"` 42 | } 43 | 44 | func (p SearchParams) Values() url.Values { 45 | return values(map[string]string{ 46 | "routing": p.Routing, 47 | "preference": p.Preference, 48 | "search_type": p.SearchType, 49 | }) 50 | } 51 | 52 | type SearchRequest struct { 53 | Params SearchParams 54 | Query SubQuery 55 | } 56 | 57 | func (r SearchRequest) EncodeMultiHeader(enc *json.Encoder) error { 58 | return enc.Encode(r.Params) 59 | } 60 | 61 | func (r SearchRequest) EncodeQuery(enc *json.Encoder) error { 62 | return enc.Encode(r.Query) 63 | } 64 | 65 | func (r SearchRequest) Request(uri *url.URL) (*http.Request, error) { 66 | uri.Path = r.Path() 67 | uri.RawQuery = r.Params.Values().Encode() 68 | 69 | buf := new(bytes.Buffer) 70 | enc := json.NewEncoder(buf) 71 | 72 | if err := r.EncodeQuery(enc); err != nil { 73 | return nil, err 74 | } 75 | 76 | return http.NewRequest("GET", uri.String(), buf) 77 | } 78 | 79 | func (r SearchRequest) Path() string { 80 | switch true { 81 | case len(r.Params.Indices) == 0 && len(r.Params.Types) == 0: 82 | return fmt.Sprintf( 83 | "/_search", // all indices, all types 84 | ) 85 | 86 | case len(r.Params.Indices) > 0 && len(r.Params.Types) == 0: 87 | return fmt.Sprintf( 88 | "/%s/_search", 89 | strings.Join(r.Params.Indices, ","), 90 | ) 91 | 92 | case len(r.Params.Indices) == 0 && len(r.Params.Types) > 0: 93 | return fmt.Sprintf( 94 | "/_all/%s/_search", 95 | strings.Join(r.Params.Types, ","), 96 | ) 97 | 98 | case len(r.Params.Indices) > 0 && len(r.Params.Types) > 0: 99 | return fmt.Sprintf( 100 | "/%s/%s/_search", 101 | strings.Join(r.Params.Indices, ","), 102 | strings.Join(r.Params.Types, ","), 103 | ) 104 | } 105 | panic("unreachable") 106 | } 107 | 108 | // 109 | // 110 | // 111 | 112 | type MultiSearchParams struct { 113 | Indices []string 114 | Types []string 115 | 116 | SearchType string 117 | } 118 | 119 | func (p MultiSearchParams) Values() url.Values { 120 | return values(map[string]string{ 121 | "search_type": p.SearchType, 122 | }) 123 | } 124 | 125 | type MultiSearchRequest struct { 126 | Params MultiSearchParams 127 | Requests []SearchRequest 128 | } 129 | 130 | func (r MultiSearchRequest) Request(uri *url.URL) (*http.Request, error) { 131 | uri.Path = "/_msearch" 132 | uri.RawQuery = r.Params.Values().Encode() 133 | 134 | buf := new(bytes.Buffer) 135 | enc := json.NewEncoder(buf) 136 | 137 | for _, req := range r.Requests { 138 | if err := req.EncodeMultiHeader(enc); err != nil { 139 | return nil, err 140 | } 141 | if err := req.EncodeQuery(enc); err != nil { 142 | return nil, err 143 | } 144 | } 145 | 146 | return http.NewRequest("GET", uri.String(), buf) 147 | } 148 | -------------------------------------------------------------------------------- /requests_test.go: -------------------------------------------------------------------------------- 1 | package elasticsearch_test 2 | 3 | import ( 4 | es "github.com/peterbourgon/elasticsearch" 5 | "io/ioutil" 6 | "net/url" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestSearchRequestPath(t *testing.T) { 12 | for _, tuple := range []struct { 13 | r es.SearchRequest 14 | expected string 15 | }{ 16 | { 17 | r: es.SearchRequest{ 18 | es.SearchParams{ 19 | Indices: []string{}, 20 | Types: []string{}, 21 | }, 22 | nil, 23 | }, 24 | expected: "/_search", 25 | }, 26 | { 27 | r: es.SearchRequest{ 28 | es.SearchParams{ 29 | Indices: []string{"i1"}, 30 | Types: []string{}, 31 | }, 32 | nil, 33 | }, 34 | expected: "/i1/_search", 35 | }, 36 | { 37 | r: es.SearchRequest{ 38 | es.SearchParams{ 39 | Indices: []string{}, 40 | Types: []string{"t1"}, 41 | }, 42 | nil, 43 | }, 44 | expected: "/_all/t1/_search", 45 | }, 46 | { 47 | r: es.SearchRequest{ 48 | es.SearchParams{ 49 | Indices: []string{"i1"}, 50 | Types: []string{"t1"}, 51 | }, 52 | nil, 53 | }, 54 | expected: "/i1/t1/_search", 55 | }, 56 | { 57 | r: es.SearchRequest{ 58 | es.SearchParams{ 59 | Indices: []string{"i1", "i2"}, 60 | Types: []string{}, 61 | }, 62 | nil, 63 | }, 64 | expected: "/i1,i2/_search", 65 | }, 66 | { 67 | r: es.SearchRequest{ 68 | es.SearchParams{ 69 | Indices: []string{}, 70 | Types: []string{"t1", "t2", "t3"}, 71 | }, 72 | nil, 73 | }, 74 | expected: "/_all/t1,t2,t3/_search", 75 | }, 76 | { 77 | r: es.SearchRequest{ 78 | es.SearchParams{ 79 | Indices: []string{"i1", "i2"}, 80 | Types: []string{"t1", "t2", "t3"}, 81 | }, 82 | nil, 83 | }, 84 | expected: "/i1,i2/t1,t2,t3/_search", 85 | }, 86 | } { 87 | if expected, got := tuple.expected, tuple.r.Path(); expected != got { 88 | t.Errorf("%v: expected '%s', got '%s'", tuple.r, expected, got) 89 | } 90 | } 91 | } 92 | 93 | func TestSearchRequestValues(t *testing.T) { 94 | for _, tuple := range []struct { 95 | r es.SearchRequest 96 | expected string 97 | }{ 98 | { 99 | r: es.SearchRequest{ 100 | Params: es.SearchParams{ 101 | Preference: "foo", 102 | }, 103 | }, 104 | expected: "preference=foo", 105 | }, 106 | } { 107 | if expected, got := tuple.expected, tuple.r.Params.Values().Encode(); expected != got { 108 | t.Errorf("%v: expected '%s', got '%s'", tuple.r, expected, got) 109 | } 110 | } 111 | } 112 | 113 | func TestMultiSearchRequestBody(t *testing.T) { 114 | m := es.MultiSearchRequest{ 115 | es.MultiSearchParams{}, 116 | []es.SearchRequest{ 117 | es.SearchRequest{ 118 | es.SearchParams{ 119 | Indices: []string{}, 120 | Types: []string{}, 121 | }, 122 | map[string]interface{}{"query": "1"}, 123 | }, 124 | es.SearchRequest{ 125 | es.SearchParams{ 126 | Indices: []string{"i1"}, 127 | Types: []string{}, 128 | }, 129 | map[string]interface{}{"query": "2"}, 130 | }, 131 | es.SearchRequest{ 132 | es.SearchParams{ 133 | Indices: []string{}, 134 | Types: []string{"t1"}, 135 | }, 136 | map[string]interface{}{"query": "3"}, 137 | }, 138 | es.SearchRequest{ 139 | es.SearchParams{ 140 | Indices: []string{"i1"}, 141 | Types: []string{"t1"}, 142 | }, 143 | map[string]interface{}{"query": "4"}, 144 | }, 145 | es.SearchRequest{ 146 | es.SearchParams{ 147 | Indices: []string{"i1", "i2"}, 148 | Types: []string{"t1", "t2", "t3"}, 149 | }, 150 | map[string]interface{}{"query": "5"}, 151 | }, 152 | }, 153 | } 154 | 155 | req, err := m.Request(&url.URL{}) 156 | 157 | if expected, got := "/_msearch", req.URL.Path; expected != got { 158 | t.Errorf("Path: expected '%s', got '%s'", expected, got) 159 | } 160 | 161 | expected := strings.Join( 162 | []string{ 163 | `{}`, 164 | `{"query":"1"}`, 165 | `{"index":["i1"]}`, 166 | `{"query":"2"}`, 167 | `{"type":["t1"]}`, 168 | `{"query":"3"}`, 169 | `{"index":["i1"],"type":["t1"]}`, 170 | `{"query":"4"}`, 171 | `{"index":["i1","i2"],"type":["t1","t2","t3"]}`, 172 | `{"query":"5"}`, 173 | }, 174 | "\n", 175 | ) + "\n" 176 | got, err := ioutil.ReadAll(req.Body) 177 | if err != nil { 178 | t.Fatal(err) 179 | } 180 | if expected != string(got) { 181 | t.Errorf("Body: expected:\n---\n%s\n---\ngot:\n---\n%s\n---\n", expected, got) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | // SearchResponse represents the response given by ElasticSearch from a search 4 | // query. 5 | type SearchResponse struct { 6 | Took int `json:"took"` // ms 7 | 8 | HitsWrapper struct { 9 | Total int `json:"total"` 10 | Hits []struct { 11 | Index string `json:"_index"` 12 | Type string `json:"_type"` 13 | ID string `json:"_id"` 14 | Score *float64 `json:"_score"` // can be 'null' with constant_score 15 | } `json:"hits,omitempty"` 16 | } `json:"hits"` 17 | 18 | Facets map[string]FacetResponse `json:"facets,omitempty"` 19 | 20 | TimedOut bool `json:"timed_out,omitempty"` 21 | Error string `json:"error,omitempty"` 22 | Status int `json:"status,omitempty"` 23 | } 24 | 25 | type FacetResponse struct { 26 | Type string `json:"_type"` 27 | Missing int64 `json:"missing"` 28 | Total int64 `json:"total"` 29 | Other int64 `json:"other"` 30 | Terms []struct { 31 | Term string `json:"term"` 32 | Count int64 `json:"count"` 33 | } `json:"terms"` 34 | } 35 | 36 | type MultiSearchResponse struct { 37 | Responses []SearchResponse `json:"responses"` 38 | } 39 | -------------------------------------------------------------------------------- /searcher.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | // Searcher is the interface that wraps the basic Search method. 4 | // Search transforms a Request into a SearchResponse (or an error). 5 | type Searcher interface { 6 | Search(SearchRequest) (SearchResponse, error) 7 | } 8 | 9 | // MultiSearcher is the interface that wraps the MultiSearch method. 10 | type MultiSearcher interface { 11 | MultiSearch(MultiSearchRequest) (MultiSearchResponse, error) 12 | } 13 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // This file contains structures that represent all of the various JSON- 8 | // marshalable queries and sub-queries that are part of the ElasticSearch 9 | // grammar. These structures are one-way: they're only meant to be Marshaled. 10 | 11 | type SubQuery interface{} 12 | 13 | var nilSubQuery SubQuery 14 | 15 | // 16 | // 17 | // 18 | 19 | // Wrapper gives a dynamic name to a SubQuery. For example, Name="foo" 20 | // Wrapped=`{"bar": 123}` marshals to `{"foo": {"bar": 123}}`. 21 | // 22 | // You *can* use this directly in your business-logic code, but if you do, it's 23 | // probably a sign you should file an issue (or make a pull request) to give 24 | // your specific use-case proper, first class support, via a FooQuery[Params] 25 | // type-set. 26 | type Wrapper struct { 27 | Name string 28 | Wrapped SubQuery 29 | } 30 | 31 | func (w *Wrapper) MarshalJSON() ([]byte, error) { 32 | return json.Marshal(map[string]SubQuery{ 33 | w.Name: w.Wrapped, 34 | }) 35 | } 36 | 37 | // 38 | // 39 | // 40 | 41 | func QueryWrapper(q SubQuery) SubQuery { 42 | return &Wrapper{ 43 | Name: "query", 44 | Wrapped: q, 45 | } 46 | } 47 | 48 | // 49 | // 50 | // 51 | 52 | // GenericQueryParams marshal to a valid query object for a large number of 53 | // query types. You generally use them applied to a particular field, ie. scope; 54 | // see FieldedGenericQuery. 55 | type GenericQueryParams struct { 56 | Query string `json:"query,omitempty"` 57 | Analyzer string `json:"analyzer,omitempty"` 58 | Type string `json:"type,omitempty"` 59 | MaxExpansions string `json:"max_expansions,omitempty"` 60 | Boost float32 `json:"boost,omitempty"` 61 | Operator string `json:"operator,omitempty"` 62 | MinimumShouldMatch string `json:"minimum_should_match,omitempty"` 63 | CutoffFrequency float32 `json:"cutoff_frequency,omitempty"` 64 | } 65 | 66 | // FieldedGenericQuery returns a SubQuery representing the passed QueryParams 67 | // applied to the given scope, ie. field. The returned SubQuery can be used as 68 | // the Query in a MatchQuery, for example. 69 | func FieldedGenericQuery(field string, p GenericQueryParams) SubQuery { 70 | return &Wrapper{ 71 | Name: field, 72 | Wrapped: p, 73 | } 74 | } 75 | 76 | // 77 | // 78 | // 79 | 80 | // http://www.elasticsearch.org/guide/reference/query-dsl/match-query.html 81 | type MatchQueryParams struct { 82 | Query SubQuery `json:"match"` 83 | } 84 | 85 | func MatchQuery(p MatchQueryParams) SubQuery { 86 | return p 87 | } 88 | 89 | // 90 | // 91 | // 92 | 93 | // http://www.elasticsearch.org/guide/reference/query-dsl/term-query.html 94 | // Typically `Query` would be &Wrapper{Name: "fieldname", Wrapped: "value"}. 95 | type TermQueryParams struct { 96 | Query SubQuery `json:"term"` 97 | } 98 | 99 | func TermQuery(p TermQueryParams) SubQuery { 100 | return p 101 | } 102 | 103 | // 104 | // 105 | // 106 | 107 | // http://www.elasticsearch.org/guide/reference/query-dsl/terms-query.html 108 | // "a simpler syntax query for using a `bool` query with several `term` queries 109 | // in the `should` clauses." 110 | type TermsQueryParams struct { 111 | Query SubQuery `json:"terms"` // often `{ "field": ["value1", "value2"] }` 112 | } 113 | 114 | func TermsQuery(p TermsQueryParams) SubQuery { 115 | return p 116 | } 117 | 118 | // 119 | // 120 | // 121 | 122 | // http://www.elasticsearch.org/guide/reference/query-dsl/dis-max-query.html 123 | type DisMaxQueryParams struct { 124 | Queries []SubQuery `json:"queries"` 125 | Boost float32 `json:"boost,omitempty"` 126 | TieBreaker float32 `json:"tie_breaker,omitempty"` 127 | } 128 | 129 | func DisMaxQuery(p DisMaxQueryParams) SubQuery { 130 | return &Wrapper{ 131 | Name: "dis_max", 132 | Wrapped: p, 133 | } 134 | } 135 | 136 | // 137 | // 138 | // 139 | 140 | // http://www.elasticsearch.org/guide/reference/query-dsl/bool-query.html 141 | type BoolQueryParams struct { 142 | Must SubQuery `json:"must,omitempty"` // can be slice! 143 | Should SubQuery `json:"should,omitempty"` 144 | MustNot SubQuery `json:"must_not,omitempty"` 145 | MinimumNumberShouldMatch int `json:"minimum_number_should_match,omitempty"` 146 | Boost float32 `json:"boost,omitempty"` 147 | } 148 | 149 | func BoolQuery(p BoolQueryParams) SubQuery { 150 | return &Wrapper{ 151 | Name: "bool", 152 | Wrapped: p, 153 | } 154 | } 155 | 156 | // 157 | // 158 | // 159 | 160 | // http://www.elasticsearch.org/guide/reference/query-dsl/custom-score-query.html 161 | type CustomScoreQueryParams struct { 162 | Script string `json:"script"` 163 | Lang string `json:"lang"` 164 | Params map[string]interface{} `json:"params"` 165 | Query SubQuery `json:"query"` 166 | } 167 | 168 | func CustomScoreQuery(p CustomScoreQueryParams) SubQuery { 169 | return &Wrapper{ 170 | Name: "custom_score", 171 | Wrapped: p, 172 | } 173 | } 174 | 175 | // 176 | // 177 | // 178 | 179 | type ConstantScoreQueryParams struct { 180 | Query SubQuery `json:"query,omitempty"` 181 | Filter FilterSubQuery `json:"filter,omitempty"` 182 | Boost float32 `json:"boost,omitempty"` 183 | } 184 | 185 | func ConstantScoreQuery(p ConstantScoreQueryParams) SubQuery { 186 | return &Wrapper{ 187 | Name: "constant_score", 188 | Wrapped: p, 189 | } 190 | } 191 | 192 | // 193 | // 194 | // 195 | 196 | func MatchAllQuery() SubQuery { 197 | return &Wrapper{ 198 | Name: "match_all", 199 | Wrapped: map[string]interface{}{}, // render to '{}' 200 | } 201 | } 202 | 203 | // 204 | // 205 | // 206 | 207 | // Haven't quite figured out how to best represent this. 208 | // TODO break these up into embeddable query-parts? 209 | type OffsetLimitFacetsFilterQueryParams struct { 210 | Offset int `json:"from"` 211 | Limit int `json:"size"` 212 | Facets FacetSubQuery `json:"facets,omitempty"` 213 | Filter FilterSubQuery `json:"filter,omitempty"` 214 | Query SubQuery `json:"query"` 215 | } 216 | 217 | // 218 | // 219 | // 220 | // ============================================================================= 221 | // HERE BE FILTERS 222 | // ============================================================================= 223 | // 224 | // 225 | // 226 | 227 | type FilterSubQuery SubQuery 228 | 229 | func MakeFilter(filter SubQuery) FilterSubQuery { 230 | return FilterSubQuery(filter) 231 | } 232 | 233 | func MakeFilters(filters []SubQuery) []FilterSubQuery { 234 | a := []FilterSubQuery{} 235 | for _, filter := range filters { 236 | a = append(a, MakeFilter(filter)) 237 | } 238 | return a 239 | } 240 | 241 | type BooleanFiltersParams struct { 242 | AndFilters []FilterSubQuery 243 | OrFilters []FilterSubQuery 244 | } 245 | 246 | func BooleanFilters(p BooleanFiltersParams) SubQuery { 247 | switch nAnd, nOr := len(p.AndFilters), len(p.OrFilters); true { 248 | case nAnd <= 0 && nOr <= 0: 249 | return map[string]interface{}{} // render to '{}' 250 | 251 | case nAnd > 0 && nOr <= 0: 252 | return &Wrapper{ 253 | Name: "and", 254 | Wrapped: p.AndFilters, 255 | } 256 | 257 | case nAnd <= 0 && nOr > 0: 258 | return &Wrapper{ 259 | Name: "or", 260 | Wrapped: p.OrFilters, 261 | } 262 | 263 | case nAnd > 0 && nOr > 0: 264 | combinedFilters := append(p.AndFilters, &Wrapper{ 265 | Name: "or", 266 | Wrapped: p.OrFilters, 267 | }) 268 | return &Wrapper{ 269 | Name: "and", 270 | Wrapped: combinedFilters, 271 | } 272 | } 273 | panic("unreachable") 274 | } 275 | 276 | // 277 | // 278 | // 279 | 280 | type QueryFilterParams struct { 281 | Query SubQuery `json:"query"` 282 | } 283 | 284 | func QueryFilter(p QueryFilterParams) FilterSubQuery { 285 | return p // no need for another layer of indirection; just like a typecast 286 | } 287 | 288 | // 289 | // 290 | // 291 | 292 | type TermFilterParams struct { 293 | Field string 294 | Value string // for multiple values, use TermsFilter 295 | } 296 | 297 | func TermFilter(p TermFilterParams) FilterSubQuery { 298 | return &Wrapper{ 299 | Name: "term", 300 | Wrapped: &Wrapper{ 301 | Name: p.Field, 302 | Wrapped: p.Value, 303 | }, 304 | } 305 | } 306 | 307 | type TermsFilterParams struct { 308 | Field string 309 | Values []string 310 | Execution string 311 | } 312 | 313 | func TermsFilter(p TermsFilterParams) FilterSubQuery { 314 | terms := map[string]interface{}{ 315 | p.Field: p.Values, 316 | } 317 | if p.Execution != "" { 318 | terms["execution"] = p.Execution 319 | } 320 | return map[string]interface{}{ 321 | "terms": terms, 322 | } 323 | } 324 | 325 | // 326 | // 327 | // 328 | 329 | // http://www.elasticsearch.org/guide/reference/query-dsl/type-filter.html 330 | type FieldedFilterParams struct { 331 | Value string `json:"value"` 332 | } 333 | 334 | // TODO I guess remove all Fielded* functions except FieldedGenericQuery? 335 | // TODO and rename FieldedGenericQuery to like FieldedSubObject, maybe? 336 | func FieldedFilter(fieldName string, p FieldedFilterParams) FilterSubQuery { 337 | return &Wrapper{ 338 | Name: fieldName, 339 | Wrapped: p, 340 | } 341 | } 342 | 343 | // 344 | // 345 | // 346 | 347 | type RangeSubQuery SubQuery 348 | 349 | // http://www.elasticsearch.org/guide/reference/query-dsl/range-filter.html 350 | type RangeFilterParams struct { 351 | From string `json:"from,omitempty"` 352 | To string `json:"to,omitempty"` 353 | IncludeLower bool `json:"include_lower"` 354 | IncludeUpper bool `json:"include_upper"` 355 | } 356 | 357 | func FieldedRangeSubQuery(field string, p RangeFilterParams) RangeSubQuery { 358 | return &Wrapper{ 359 | Name: field, 360 | Wrapped: p, 361 | } 362 | } 363 | 364 | func RangeFilter(q RangeSubQuery) FilterSubQuery { 365 | return &Wrapper{ 366 | Name: "range", 367 | Wrapped: q, 368 | } 369 | } 370 | 371 | // 372 | // 373 | // 374 | // ============================================================================= 375 | // HERE BE FACETS 376 | // ============================================================================= 377 | // 378 | // 379 | // 380 | 381 | type FacetSubQuery SubQuery 382 | 383 | // http://www.elasticsearch.org/guide/reference/api/search/facets/terms-facet.html 384 | type TermsFacetParams struct { 385 | Field string `json:"field"` 386 | Size int `json:"size"` 387 | } 388 | 389 | func TermsFacet(p TermsFacetParams) FacetSubQuery { 390 | return &Wrapper{ 391 | Name: "terms", 392 | Wrapped: p, 393 | } 394 | } 395 | 396 | // TODO other types of facets 397 | 398 | // NamedFacet wraps any FooFacet SubQuery so that it can be used 399 | // wherever a facet is called for. 400 | func NamedFacet(name string, q FacetSubQuery) FacetSubQuery { 401 | return &Wrapper{ 402 | Name: name, 403 | Wrapped: q, 404 | } 405 | } 406 | --------------------------------------------------------------------------------