go-elasticsearch icon indicating copy to clipboard operation
go-elasticsearch copied to clipboard

[ENHANCEMENT] Search results helper

Open karmi opened this issue 4 years ago • 3 comments

The client should provide a high-level helper component for convenient, efficient handling of search results.

It should provide the following facilities:

  • Easy iteration over search results (metadata as well as hits)
  • Easy access to the _source field
  • Support for all hit properties (sort, highlight, ...)
  • Support for scrolling the search
  • Support for injecting a custom JSON decoder

Example:

res, err := es.Search(
// ...

results, _ := NewSearchResponse(res.Body)

log.Println("Total hits:", results.Hits.Total())

for results.Hits.Next() {
	item := results.Hits.Item()
	fmt.Printf("* %s \n", item.Source["title"])
}
Example implementation

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"strings"

	"github.com/elastic/go-elasticsearch/v8"
	"github.com/elastic/go-elasticsearch/v8/estransport"
	"github.com/elastic/go-elasticsearch/v8/esutil"
)

func main() {
	log.SetFlags(0)

	var indexName = "test-search"

	es, _ := elasticsearch.NewClient(elasticsearch.Config{
		Logger: &estransport.ColorLogger{
			Output:             os.Stdout,
			EnableRequestBody:  false,
			EnableResponseBody: false,
		},
	})

	bi, err := esutil.NewBulkIndexer(esutil.BulkIndexerConfig{
		Index:  indexName,
		Client: es,
	})
	if err != nil {
		log.Fatalf("Error creating the indexer: %s", err)
	}

	log.Println("Indexing the documents...")
	es.Indices.Delete([]string{indexName})
	for i := 1; i <= 15; i++ {
		bi.Add(
			context.Background(),
			esutil.BulkIndexerItem{
				Action: "index",
				Body:   strings.NewReader(fmt.Sprintf(`{"title" : "Test %03d"}`, i)),
			},
		)
	}
	bi.Close(context.Background())
	es.Indices.Refresh(es.Indices.Refresh.WithIndex(indexName))

	log.Println(strings.Repeat("-", 80))
	log.Println("Searching the index...")
	res, err := es.Search(
		es.Search.WithIndex(indexName),
		es.Search.WithSize(12),
	)
	if err != nil {
		log.Fatalf("ERROR: %s", err)
	}
	defer res.Body.Close()

	if res.IsError() {
		log.Fatalf("ERROR: %s", res.Status())
	}

	results, err := NewSearchResponse(res.Body)
	if err != nil {
		log.Fatalf("ERROR: %s", err)
	}

	log.Println("Total hits:", results.Hits.Total())

	for results.Hits.Next() {
		item := results.Hits.Item()
		fmt.Printf("* %s \n", item.Source["title"])
	}
}

// ----------------------------------------------------------------------------

func NewSearchResponse(body io.Reader) (SearchResponse, error) {
	var response = SearchResponse{
		body: body,
		Hits: &SearchResponseHits{},
	}

	var r envelopeResponse
	if err := json.NewDecoder(body).Decode(&r); err != nil {
		return response, err
	}

	response.Hits.total = r.Hits.Total.Value

	for _, h := range r.Hits.Hits {
		var hit SearchResponseHit
		hit.ID = h.ID
		hit.Index = h.Index
		hit.Source = make(map[string]interface{})

		if err := json.Unmarshal(h.Source, &hit.Source); err != nil {
			return response, err
		}

		response.Hits.append(hit)
	}

	return response, nil
}

type SearchResponse struct {
	body io.Reader
	Hits *SearchResponseHits
}

type SearchResponseHits struct {
	hits         []SearchResponseHit
	total        int
	currentIndex int
}

type SearchResponseHit struct {
	Index  string
	ID     string
	Source map[string]interface{}
}

func (h *SearchResponseHits) Total() int {
	return h.total
}

func (h *SearchResponseHits) Next() bool {
	if h.currentIndex < len(h.hits) {
		h.currentIndex++
		return true
	}
	h.currentIndex = 0
	return false
}

func (h *SearchResponseHits) Item() SearchResponseHit {
	return h.hits[h.currentIndex-1]
}

func (h *SearchResponseHits) append(hit SearchResponseHit) {
	h.hits = append(h.hits, hit)
}

type envelopeResponse struct {
	Took int
	Hits struct {
		Total struct{ Value int }
		Hits  []struct {
			Index  string          `json:"_index"`
			ID     string          `json:"_id"`
			Source json.RawMessage `json:"_source"`
		}
	}
}

karmi avatar Sep 23 '20 17:09 karmi

Hey @karmi, I'm interested in implementing the mentioned features. Would you please guide me from where should I start? I'm determined but new here. I'd love to work on this issue. Thanks.

bisakhmondal avatar Oct 01 '20 18:10 bisakhmondal

Hello, thanks for the offer. I'm not sure that this issue is best for starting with adding features to the package. It assumes a lot of familiarity with Elasticsearch and common usage patterns, figuring out how to keep any supporting structures in sync with the Elasticsearch response format evolution, and so on. A more approachable "good first issue" is adding support for metadata to the bulk indexing helper, see eg. https://github.com/elastic/go-elasticsearch/issues/175 and related issues / pull requests. (That said, if you want to play with the feature, I can look at patches, of course.)

karmi avatar Oct 06 '20 07:10 karmi

Thanks for the Suggestion. Yeah, it's better to contribute to a few good first issues first. Thanks.

bisakhmondal avatar Oct 09 '20 12:10 bisakhmondal